History

api+ui: expose scope in writes + CreatableSecretPicker (#334)

- scope handler: surfaces outermost scope (executor.scopes.at(-1)) instead of the old single scope. - secrets handler: set takes path.scopeId and threads it as SetSecretInput.scope so the executor writes at the right scope. - secret-header-auth: exports CreatableSecretPicker + InlineCreateSecret for reuse by the OpenAPI connections pane.

Rhys Sullivan committed 6 months ago

Commit 793e2db2 parents 5a05685 5ac514d3 files · +68 −12Browse files

Unified · Split

3 files
  1. packages/core/api/src/handlers/scope.ts +8 −3
  2. packages/core/api/src/handlers/secrets.ts +11 −8
  3. packages/react/src/plugins/secret-header-auth.tsx +49 −1
packages/core/api/src/handlers/scope.ts+8−3
@@ -9,10 +9,15 @@ export const ScopeHandlers = HttpApiBuilder.group(ExecutorApi, "scope", (handler · 8 unmodified lines
9 handlers.handle("info", () =>9 handlers.handle("info", () =>
10 capture(Effect.gen(function* () {10 capture(Effect.gen(function* () {
11 const executor = yield* ExecutorService;11 const executor = yield* ExecutorService;
12 // Outermost scope (organization / workspace). A follow-up that
13 // exposes per-user stacks end-to-end can extend this response
14 // with the full list; for now, single-scope deployments and the
15 // current `[org]` cloud setup see identical output.
16 const scope = executor.scopes.at(-1)!;
12 return {17 return {
13 id: executor.scope.id,18 id: scope.id,
14 name: executor.scope.name,19 name: scope.name,
15 dir: executor.scope.name,20 dir: scope.name,
16 };21 };
17 })),22 })),
18 ),23 ),
packages/core/api/src/handlers/secrets.ts+11−8
@@ -1,6 +1,6 @@
1import { HttpApiBuilder } from "@effect/platform";1import { HttpApiBuilder } from "@effect/platform";
2import { Effect } from "effect";2import { Effect } from "effect";
3import { SecretNotFoundError, type SecretRef } from "@executor/sdk";3import { SecretNotFoundError, SetSecretInput, type SecretRef } from "@executor/sdk";
4 4
5import { ExecutorApi } from "../api";5import { ExecutorApi } from "../api";
6import { ExecutorService } from "../services";6import { ExecutorService } from "../services";
@@ -30,15 +30,18 @@ export const SecretsHandlers = HttpApiBuilder.group(ExecutorApi, "secrets", (han · 23 unmodified lines
30 return { secretId: path.secretId, status };30 return { secretId: path.secretId, status };
31 })),31 })),
32 )32 )
33 .handle("set", ({ payload }) =>33 .handle("set", ({ path, payload }) =>
34 capture(Effect.gen(function* () {34 capture(Effect.gen(function* () {
35 const executor = yield* ExecutorService;35 const executor = yield* ExecutorService;
36 const ref = yield* executor.secrets.set({36 const ref = yield* executor.secrets.set(
37 id: payload.id,37 new SetSecretInput({
38 name: payload.name,38 id: payload.id,
39 value: payload.value,39 scope: path.scopeId,
40 provider: payload.provider,40 name: payload.name,
41 });41 value: payload.value,
42 provider: payload.provider,
43 }),
44 );
42 return refToResponse(ref);45 return refToResponse(ref);
43 })),46 })),
44 )47 )
packages/react/src/plugins/secret-header-auth.tsx+49−1
@@ -68,7 +68,7 @@ function slugifyForSecretId(input: string): string { · 67 unmodified lines
68 .replace(/^-+|-+$/g, "");68 .replace(/^-+|-+$/g, "");
69}69}
70 70
71function InlineCreateSecret(props: {71export function InlineCreateSecret(props: {
72 suggestedId: string;72 suggestedId: string;
73 suggestedName: string;73 suggestedName: string;
74 onCreated: (secretId: string) => void;74 onCreated: (secretId: string) => void;
@@ -416,3 +416,51 @@ export function SecretHeaderAuthRow(props: { · 341 unmodified lines
416 </div>416 </div>
417 );417 );
418}418}
419
420// ---------------------------------------------------------------------------
421// CreatableSecretPicker — SecretPicker + inline "+ New secret" create flow
422// ---------------------------------------------------------------------------
423
424export function CreatableSecretPicker(props: {
425 readonly value: string | null;
426 readonly onSelect: (secretId: string) => void;
427 readonly secrets: readonly SecretPickerSecret[];
428 readonly placeholder?: string;
429 /**
430 * Display name of the source the secret belongs to (e.g. "Stripe").
431 * Combined with `secretLabel` to produce a suggested name/ID.
432 */
433 readonly sourceName?: string;
434 /** Role of this secret (e.g. "Client ID", "API Token"). */
435 readonly secretLabel: string;
436}) {
437 const { value, onSelect, secrets, placeholder, sourceName, secretLabel } = props;
438 const [creating, setCreating] = useState(false);
439
440 const suggestedName = [sourceName?.trim(), secretLabel].filter(Boolean).join(" ");
441 const suggestedId = slugifyForSecretId(suggestedName) || "secret";
442
443 if (creating) {
444 return (
445 <InlineCreateSecret
446 suggestedId={suggestedId}
447 suggestedName={suggestedName}
448 onCreated={(id) => {
449 onSelect(id);
450 setCreating(false);
451 }}
452 onCancel={() => setCreating(false)}
453 />
454 );
455 }
456
457 return (
458 <SecretPicker
459 value={value}
460 onSelect={onSelect}
461 secrets={secrets}
462 placeholder={placeholder}
463 onCreateNew={() => setCreating(true)}
464 />
465 );
466}