History

Use shared secret picker for Google Discovery (#450)

Rhys Sullivan committed 6 months ago

Commit 9638cc31 parent a99aceb1 file · +8 −123Browse files

Unified · Split

packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx+8−123
@@ -1,12 +1,12 @@
1import { useCallback, useEffect, useMemo, useRef, useState } from "react";1import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2import { useAtomSet, useAtomValue, Result } from "@effect-atom/atom-react";2import { useAtomSet, useAtomValue, Result } from "@effect-atom/atom-react";
3 3
4import { secretsAtom, setSecret } from "@executor/react/api/atoms";4import { secretsAtom } from "@executor/react/api/atoms";
5import { usePendingSources } from "@executor/react/api/optimistic";5import { usePendingSources } from "@executor/react/api/optimistic";
6import { secretWriteKeys, sourceWriteKeys } from "@executor/react/api/reactivity-keys";6import { sourceWriteKeys } from "@executor/react/api/reactivity-keys";
7import { useScope } from "@executor/react/api/scope-context";7import { useScope } from "@executor/react/api/scope-context";
8import { SecretPicker, type SecretPickerSecret } from "@executor/react/plugins/secret-picker";8import type { SecretPickerSecret } from "@executor/react/plugins/secret-picker";
9import { SecretId } from "@executor/sdk";9import { CreatableSecretPicker } from "@executor/react/plugins/secret-header-auth";
10import { Badge } from "@executor/react/components/badge";10import { Badge } from "@executor/react/components/badge";
11import { Button } from "@executor/react/components/button";11import { Button } from "@executor/react/components/button";
12import {12import {
@@ -42,7 +42,6 @@ import { · 29 unmodified lines
42import { FilterTabs } from "@executor/react/components/filter-tabs";42import { FilterTabs } from "@executor/react/components/filter-tabs";
43import { FloatActions } from "@executor/react/components/float-actions";43import { FloatActions } from "@executor/react/components/float-actions";
44import { Input } from "@executor/react/components/input";44import { Input } from "@executor/react/components/input";
45import { Label } from "@executor/react/components/label";
46import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group";45import { RadioGroup, RadioGroupItem } from "@executor/react/components/radio-group";
47import { IOSSpinner, Spinner } from "@executor/react/components/spinner";46import { IOSSpinner, Spinner } from "@executor/react/components/spinner";
48import { addGoogleDiscoverySource, probeGoogleDiscovery } from "./atoms";47import { addGoogleDiscoverySource, probeGoogleDiscovery } from "./atoms";
@@ -63,97 +62,6 @@ const GOOGLE_EXTRA_AUTHORIZATION_PARAMS = { · 14 unmodified lines
63 62
64type GoogleAuthKind = "none" | "oauth2";63type GoogleAuthKind = "none" | "oauth2";
65 64
66// ---------------------------------------------------------------------------
67// Inline secret creation
68// ---------------------------------------------------------------------------
69
70function 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 creation66// Client secret field with inline creation
159// ---------------------------------------------------------------------------67// ---------------------------------------------------------------------------
@@ -161,48 +69,27 @@ function InlineCreateSecret(props: { · 1 unmodified line
161function SecretBackedField(props: {69function 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 <SecretPicker84 <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 Clear95 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 <SecretBackedField613 <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 <SecretBackedField622 <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}