Skip to content

Commit 3f51d05

Browse files
committed
feat: inline secret creation for Google Discovery OAuth setup
- Add InlineCreateSecret component to create secrets during OAuth config - Add ClientSecretField with '+ New' button next to SecretPicker - Always rebuild web dist and set NODE_ENV=production in CLI build - Remove version badge from sidebar
1 parent d38a89f commit 3f51d05

3 files changed

Lines changed: 167 additions & 33 deletions

File tree

apps/cli/src/build.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,14 @@ const isCurrentPlatform = (t: Target) =>
8282

8383
const buildWeb = async () => {
8484
const webDist = join(webRoot, "dist");
85-
if (existsSync(webDist)) return webDist;
85+
await rm(webDist, { recursive: true, force: true });
8686

8787
console.log("Building web app...");
88-
const proc = Bun.spawn(["bun", "run", "build"], { cwd: webRoot, stdio: ["ignore", "inherit", "inherit"] });
88+
const proc = Bun.spawn(["bun", "run", "build"], {
89+
cwd: webRoot,
90+
stdio: ["ignore", "inherit", "inherit"],
91+
env: { ...process.env, NODE_ENV: "production" },
92+
});
8993
if ((await proc.exited) !== 0) throw new Error("Web build failed");
9094
return webDist;
9195
};

apps/web/src/shell.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,6 @@ function SidebarContent(props: {
283283
<span className="font-display text-base tracking-tight text-foreground">
284284
executor
285285
</span>
286-
<span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary">
287-
v4
288-
</span>
289286
</Link>
290287
</div>
291288
)}
@@ -413,9 +410,6 @@ export function Shell() {
413410
<span className="font-display text-base tracking-tight text-foreground">
414411
executor
415412
</span>
416-
<span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary">
417-
v4
418-
</span>
419413
</Link>
420414
<button
421415
type="button"
@@ -458,9 +452,6 @@ export function Shell() {
458452
<span className="font-display text-base tracking-tight text-foreground">
459453
executor
460454
</span>
461-
<span className="rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium text-primary">
462-
v4
463-
</span>
464455
</Link>
465456
<div className="w-8 shrink-0" />
466457
</div>

packages/plugins/google-discovery/src/react/AddGoogleDiscoverySource.tsx

Lines changed: 161 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { 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

44
import {
55
useScope,
66
SecretPicker,
77
secretsAtom,
8+
setSecret,
89
type SecretPickerSecret,
910
} from "@executor/react";
11+
import { SecretId } from "@executor/sdk";
1012
import { Badge } from "@executor/ui/components/badge";
1113
import { Button } from "@executor/ui/components/button";
1214
import {
@@ -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+
27182
type 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

Comments
 (0)