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
99 handlers.handle("info", () =>
1010 capture(Effect.gen(function* () {
1111 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)!;
1217 return {
13− id: executor.scope.id,
14− name: executor.scope.name,
15− dir: executor.scope.name,
18+ id: scope.id,
19+ name: scope.name,
20+ dir: scope.name,
1621 };
1722 })),
1823 ),
packages/core/api/src/handlers/secrets.ts+11−8
@@ -1,6 +1,6 @@
11 import { HttpApiBuilder } from "@effect/platform";
22 import { Effect } from "effect";
3−import { SecretNotFoundError, type SecretRef } from "@executor/sdk";
3+import { SecretNotFoundError, SetSecretInput, type SecretRef } from "@executor/sdk";
44
55 import { ExecutorApi } from "../api";
66 import { ExecutorService } from "../services";
@@ -30,15 +30,18 @@ export const SecretsHandlers = HttpApiBuilder.group(ExecutorApi, "secrets", (han · 23 unmodified lines
3030 return { secretId: path.secretId, status };
3131 })),
3232 )
33− .handle("set", ({ payload }) =>
33+ .handle("set", ({ path, payload }) =>
3434 capture(Effect.gen(function* () {
3535 const executor = yield* ExecutorService;
36− const ref = yield* executor.secrets.set({
37− id: payload.id,
38− name: payload.name,
39− value: payload.value,
40− provider: payload.provider,
41− });
36+ const ref = yield* executor.secrets.set(
37+ new SetSecretInput({
38+ id: payload.id,
39+ scope: path.scopeId,
40+ name: payload.name,
41+ value: payload.value,
42+ provider: payload.provider,
43+ }),
44+ );
4245 return refToResponse(ref);
4346 })),
4447 )
packages/react/src/plugins/secret-header-auth.tsx+49−1
@@ -68,7 +68,7 @@ function slugifyForSecretId(input: string): string { · 67 unmodified lines
6868 .replace(/^-+|-+$/g, "");
6969 }
7070
71−function InlineCreateSecret(props: {
71+export function InlineCreateSecret(props: {
7272 suggestedId: string;
7373 suggestedName: string;
7474 onCreated: (secretId: string) => void;
@@ -416,3 +416,51 @@ export function SecretHeaderAuthRow(props: { · 341 unmodified lines
416416 </div>
417417 );
418418 }
419+
420+// ---------------------------------------------------------------------------
421+// CreatableSecretPicker — SecretPicker + inline "+ New secret" create flow
422+// ---------------------------------------------------------------------------
423+
424+export 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+}