Unified · Split
| @@ -1,12 +1,12 @@ | |||
| 1 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 1 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 2 | import { useAtomSet, useAtomValue, Result } from "@effect-atom/atom-react"; | 2 | import { useAtomSet, useAtomValue, Result } from "@effect-atom/atom-react"; |
| 3 | 3 | ||
| 4 | import { secretsAtom, setSecret } from "@executor/react/api/atoms"; | 4 | import { secretsAtom } from "@executor/react/api/atoms"; |
| 5 | import { usePendingSources } from "@executor/react/api/optimistic"; | 5 | import { usePendingSources } from "@executor/react/api/optimistic"; |
| 6 | import { secretWriteKeys, sourceWriteKeys } from "@executor/react/api/reactivity-keys"; | 6 | import { sourceWriteKeys } from "@executor/react/api/reactivity-keys"; |
| 7 | import { useScope } from "@executor/react/api/scope-context"; | 7 | import { useScope } from "@executor/react/api/scope-context"; |
| 8 | import { SecretPicker, type SecretPickerSecret } from "@executor/react/plugins/secret-picker"; | 8 | import type { SecretPickerSecret } from "@executor/react/plugins/secret-picker"; |
| 9 | import { SecretId } from "@executor/sdk"; | 9 | import { CreatableSecretPicker } from "@executor/react/plugins/secret-header-auth"; |
| 10 | import { Badge } from "@executor/react/components/badge"; | 10 | import { Badge } from "@executor/react/components/badge"; |
| 11 | import { Button } from "@executor/react/components/button"; | 11 | import { Button } from "@executor/react/components/button"; |
| 12 | import { | 12 | import { |
| @@ -42,7 +42,6 @@ import { · 29 unmodified lines | |||
| 42 | import { FilterTabs } from "@executor/react/components/filter-tabs"; | 42 | import { FilterTabs } from "@executor/react/components/filter-tabs"; |
| 43 | import { FloatActions } from "@executor/react/components/float-actions"; | 43 | import { FloatActions } from "@executor/react/components/float-actions"; |
| 44 | import { Input } from "@executor/react/components/input"; | 44 | import { Input } from "@executor/react/components/input"; |
| 45 | import { Label } from "@executor/react/components/label"; | ||
| 46 | import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group"; | 45 | import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group"; |
| 47 | import { IOSSpinner, Spinner } from "@executor/react/components/spinner"; | 46 | import { IOSSpinner, Spinner } from "@executor/react/components/spinner"; |
| 48 | import { addGoogleDiscoverySource, probeGoogleDiscovery } from "./atoms"; | 47 | import { addGoogleDiscoverySource, probeGoogleDiscovery } from "./atoms"; |
| @@ -63,97 +62,6 @@ const GOOGLE_EXTRA_AUTHORIZATION_PARAMS = { · 14 unmodified lines | |||
| 63 | 62 | ||
| 64 | type GoogleAuthKind = "none" | "oauth2"; | 63 | type GoogleAuthKind = "none" | "oauth2"; |
| 65 | 64 | ||
| 66 | // --------------------------------------------------------------------------- | ||
| 67 | // Inline secret creation | ||
| 68 | // --------------------------------------------------------------------------- | ||
| 69 | |||
| 70 | function InlineCreateSecret(props: { | ||
| 71 | headerName: string; | ||
| 72 | suggestedId: string; | ||
| 73 | onCreated: (secretId: string) => void; | ||
| 74 | onCancel: () => void; | ||
| 75 | }) { | ||
| 76 | const [secretId, setSecretIdValue] = useState(props.suggestedId); | ||
| 77 | const [secretName, setSecretName] = useState(props.headerName); | ||
| 78 | const [secretValue, setSecretValue] = useState(""); | ||
| 79 | const [saving, setSaving] = useState(false); | ||
| 80 | const [error, setError] = useState<string | null>(null); | ||
| 81 | const scopeId = useScope(); | ||
| 82 | const doSet = useAtomSet(setSecret, { mode: "promise" }); | ||
| 83 | |||
| 84 | const handleSave = async () => { | ||
| 85 | if (!secretId.trim() || !secretValue.trim()) return; | ||
| 86 | setSaving(true); | ||
| 87 | setError(null); | ||
| 88 | try { | ||
| 89 | await doSet({ | ||
| 90 | path: { scopeId }, | ||
| 91 | payload: { | ||
| 92 | id: SecretId.make(secretId.trim()), | ||
| 93 | name: secretName.trim() || secretId.trim(), | ||
| 94 | value: secretValue.trim(), | ||
| 95 | }, | ||
| 96 | reactivityKeys: [...secretWriteKeys], | ||
| 97 | }); | ||
| 98 | props.onCreated(secretId.trim()); | ||
| 99 | } catch (e) { | ||
| 100 | setError(e instanceof Error ? e.message : "Failed to save secret"); | ||
| 101 | setSaving(false); | ||
| 102 | } | ||
| 103 | }; | ||
| 104 | |||
| 105 | return ( | ||
| 106 | <div className="rounded-lg border border-primary/20 bg-primary/[0.02] p-3 space-y-2.5"> | ||
| 107 | <p className="text-[11px] font-semibold text-primary tracking-wide uppercase">New secret</p> | ||
| 108 | <div className="grid grid-cols-2 gap-2"> | ||
| 109 | <div className="space-y-1"> | ||
| 110 | <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">ID</Label> | ||
| 111 | <Input | ||
| 112 | value={secretId} | ||
| 113 | onChange={(e) => setSecretIdValue((e.target as HTMLInputElement).value)} | ||
| 114 | placeholder="google-client-secret" | ||
| 115 | className="h-8 text-xs font-mono" | ||
| 116 | /> | ||
| 117 | </div> | ||
| 118 | <div className="space-y-1"> | ||
| 119 | <Label className="text-[10px] uppercase tracking-wider text-muted-foreground"> | ||
| 120 | Label | ||
| 121 | </Label> | ||
| 122 | <Input | ||
| 123 | value={secretName} | ||
| 124 | onChange={(e) => setSecretName((e.target as HTMLInputElement).value)} | ||
| 125 | placeholder="Client Secret" | ||
| 126 | className="h-8 text-xs" | ||
| 127 | /> | ||
| 128 | </div> | ||
| 129 | </div> | ||
| 130 | <div className="space-y-1"> | ||
| 131 | <Label className="text-[10px] uppercase tracking-wider text-muted-foreground">Value</Label> | ||
| 132 | <Input | ||
| 133 | type="password" | ||
| 134 | value={secretValue} | ||
| 135 | onChange={(e) => setSecretValue((e.target as HTMLInputElement).value)} | ||
| 136 | placeholder="paste your client secret…" | ||
| 137 | className="h-8 text-xs font-mono" | ||
| 138 | /> | ||
| 139 | </div> | ||
| 140 | {error && <p className="text-[11px] text-destructive">{error}</p>} | ||
| 141 | <div className="flex justify-end gap-1.5 pt-0.5"> | ||
| 142 | <Button variant="outline" size="xs" onClick={props.onCancel}> | ||
| 143 | Cancel | ||
| 144 | </Button> | ||
| 145 | <Button | ||
| 146 | size="xs" | ||
| 147 | onClick={handleSave} | ||
| 148 | disabled={!secretId.trim() || !secretValue.trim() || saving} | ||
| 149 | > | ||
| 150 | {saving ? "Saving…" : "Create and use"} | ||
| 151 | </Button> | ||
| 152 | </div> | ||
| 153 | </div> | ||
| 154 | ); | ||
| 155 | } | ||
| 156 | |||
| 157 | // --------------------------------------------------------------------------- | 65 | // --------------------------------------------------------------------------- |
| 158 | // Client secret field with inline creation | 66 | // Client secret field with inline creation |
| 159 | // --------------------------------------------------------------------------- | 67 | // --------------------------------------------------------------------------- |
| @@ -161,48 +69,27 @@ function InlineCreateSecret(props: { · 1 unmodified line | |||
| 161 | function SecretBackedField(props: { | 69 | function SecretBackedField(props: { |
| 162 | label: string; | 70 | label: string; |
| 163 | suggestedSecretId: string; | 71 | suggestedSecretId: string; |
| 164 | headerName: string; | ||
| 165 | secretId: string | null; | 72 | secretId: string | null; |
| 166 | onSelect: (secretId: string | null) => void; | 73 | onSelect: (secretId: string | null) => void; |
| 167 | secretList: readonly SecretPickerSecret[]; | 74 | secretList: readonly SecretPickerSecret[]; |
| 168 | placeholder: string; | 75 | placeholder: string; |
| 169 | clearable?: boolean; | 76 | clearable?: boolean; |
| 170 | }) { | 77 | }) { |
| 171 | const [creating, setCreating] = useState(false); | ||
| 172 | const { label, secretId, onSelect, secretList, placeholder, clearable = true } = props; | 78 | const { label, secretId, onSelect, secretList, placeholder, clearable = true } = props; |
| 173 | 79 | ||
| 174 | if (creating) { | ||
| 175 | return ( | ||
| 176 | <div className="space-y-2"> | ||
| 177 | <Label>{label}</Label> | ||
| 178 | <InlineCreateSecret | ||
| 179 | headerName={props.headerName} | ||
| 180 | suggestedId={props.suggestedSecretId} | ||
| 181 | onCreated={(id) => { | ||
| 182 | onSelect(id); | ||
| 183 | setCreating(false); | ||
| 184 | }} | ||
| 185 | onCancel={() => setCreating(false)} | ||
| 186 | /> | ||
| 187 | </div> | ||
| 188 | ); | ||
| 189 | } | ||
| 190 | |||
| 191 | return ( | 80 | return ( |
| 192 | <div className="space-y-2"> | 81 | <div className="space-y-2"> |
| 193 | <Label>{label}</Label> | ||
| 194 | <div className="flex items-center gap-2"> | 82 | <div className="flex items-center gap-2"> |
| 195 | <div className="flex-1 min-w-0"> | 83 | <div className="flex-1 min-w-0"> |
| 196 | <SecretPicker | 84 | <CreatableSecretPicker |
| 197 | value={secretId} | 85 | value={secretId} |
| 198 | onSelect={onSelect} | 86 | onSelect={(id) => onSelect(id)} |
| 199 | secrets={secretList} | 87 | secrets={secretList} |
| 200 | placeholder={placeholder} | 88 | placeholder={placeholder} |
| 89 | suggestedId={props.suggestedSecretId} | ||
| 90 | secretLabel={label} | ||
| 201 | /> | 91 | /> |
| 202 | </div> | 92 | </div> |
| 203 | <Button variant="outline" size="sm" className="shrink-0" onClick={() => setCreating(true)}> | ||
| 204 | + New | ||
| 205 | </Button> | ||
| 206 | {clearable && secretId && ( | 93 | {clearable && secretId && ( |
| 207 | <Button variant="outline" onClick={() => onSelect(null)}> | 94 | <Button variant="outline" onClick={() => onSelect(null)}> |
| 208 | Clear | 95 | Clear |
| @@ -725,7 +612,6 @@ export default function AddGoogleDiscoverySource(props: { · 516 unmodified lines | |||
| 725 | <div className="space-y-3 rounded-xl border border-border bg-card px-4 py-4"> | 612 | <div className="space-y-3 rounded-xl border border-border bg-card px-4 py-4"> |
| 726 | <SecretBackedField | 613 | <SecretBackedField |
| 727 | label="OAuth Client ID" | 614 | label="OAuth Client ID" |
| 728 | headerName="Client ID" | ||
| 729 | suggestedSecretId="google-oauth-client-id" | 615 | suggestedSecretId="google-oauth-client-id" |
| 730 | secretId={clientIdSecretId} | 616 | secretId={clientIdSecretId} |
| 731 | onSelect={setClientIdSecretId} | 617 | onSelect={setClientIdSecretId} |
| @@ -735,7 +621,6 @@ export default function AddGoogleDiscoverySource(props: { · 3 unmodified lines | |||
| 735 | /> | 621 | /> |
| 736 | <SecretBackedField | 622 | <SecretBackedField |
| 737 | label="OAuth Client Secret" | 623 | label="OAuth Client Secret" |
| 738 | headerName="Client Secret" | ||
| 739 | suggestedSecretId="google-oauth-client-secret" | 624 | suggestedSecretId="google-oauth-client-secret" |
| 740 | secretId={clientSecretSecretId} | 625 | secretId={clientSecretSecretId} |
| 741 | onSelect={setClientSecretSecretId} | 626 | onSelect={setClientSecretSecretId} |