Open History to see the last commit
1import { useState, type ReactNode } from "react";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4import {
5 acceptsSecretReference,
6 isSecretReference,
7 isSecretSetting,
8 type EnvironmentId,
9 type ProviderSettingsFormAnnotation,
10 type ProviderSettingsFormControl,
11 type ProviderSettingsFormOption,
12 type ProviderSettingsFormSchemaAnnotation,
13 type SecretReference,
14} from "@t3tools/contracts";
15import { PlusIcon, XIcon } from "lucide-react";
16
17import { cn } from "../../lib/utils";
18import { Button } from "../ui/button";
19import { DraftInput } from "../ui/draft-input";
20import { Input } from "../ui/input";
21import { SecretInput } from "./SecretInput";
22import { SecretSourceInput, useSecretSources } from "./SecretSourceInput";
23import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
24import { Switch } from "../ui/switch";
25import { Textarea } from "../ui/textarea";
26import type { ProviderClientDefinition } from "@t3tools/provider-core/client";
27
28export type SettingsFormDefinition = Pick<ProviderClientDefinition, "settingsSchema"> & {
29 readonly driverKind?: ProviderClientDefinition["driverKind"];
30};
31import { SettingsRow } from "./settingsLayout";
32
33export interface ProviderSettingsFieldModel {
34 readonly key: string;
35 readonly control: ProviderSettingsFormControl;
36 readonly label: string;
37 readonly description?: string | undefined;
38 readonly placeholder?: string | undefined;
39 readonly clearWhenEmpty: "omit" | "persist";
40 /** Kept in the server's secret store: a saved value arrives as `SECRET_REDACTED` and never shows. */
41 readonly secret?: boolean | undefined;
42 /** Typed `SettingsSecretValue`: may name a secret provider reference instead of a value. */
43 readonly acceptsSecretReference?: boolean | undefined;
44 readonly defaultBooleanValue?: boolean | undefined;
45 /** Whether the field's schema decodes a typed string; one it rejects is never stored. */
46 readonly accepts?: ((value: string) => boolean) | undefined;
47
48 readonly options?: ReadonlyArray<ProviderSettingsFormOption> | undefined;
49}
50
51function titleizeFieldKey(key: string): string {
52 return key
53 .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
54 .replace(/[-_]+/g, " ")
55 .replace(/^./, (char) => char.toUpperCase());
56}
57
58function readFieldAnnotations(
59 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],
60) {
61 return Schema.resolveAnnotationsKey(fieldSchema) ?? Schema.resolveAnnotations(fieldSchema);
62}
63
64function readFieldAnnotationString(
65 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],
66 key: "title" | "description",
67): string | undefined {
68 const annotations = readFieldAnnotations(fieldSchema);
69 const value = annotations?.[key];
70 return typeof value === "string" ? value : undefined;
71}
72
73function readProviderSettingsFormAnnotation(
74 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],
75): ProviderSettingsFormAnnotation {
76 const annotation = readFieldAnnotations(fieldSchema)?.providerSettingsForm;
77 return annotation ?? {};
78}
79
80function readProviderSettingsFormSchemaAnnotation(
81 definition: SettingsFormDefinition,
82): ProviderSettingsFormSchemaAnnotation {
83 return Schema.resolveAnnotations(definition.settingsSchema)?.providerSettingsFormSchema ?? {};
84}
85
86function readFieldBooleanDefault(
87 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],
88): boolean | undefined {
89 const decodeDefault = Schema.decodeUnknownOption(fieldSchema as Schema.Decoder<unknown>);
90 const decoded = decodeDefault(undefined);
91 return Option.isSome(decoded) && typeof decoded.value === "boolean" ? decoded.value : undefined;
92}
93
94function fieldAcceptsString(
95 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],
96): (value: string) => boolean {
97 const decode = Schema.decodeUnknownOption(fieldSchema as Schema.Decoder<unknown>);
98 return (value) => Option.isSome(decode(value));
99}
100
101export function deriveProviderSettingsFields(
102 definition: SettingsFormDefinition,
103 value?: unknown,
104): ReadonlyArray<ProviderSettingsFieldModel> {
105 const isLocalAcp =
106 definition.driverKind === "acpRegistry" &&
107 readProviderConfigString(value, "source") === "local";
108 const schemaAnnotation = readProviderSettingsFormSchemaAnnotation(definition);
109 const orderedKeys = new Map(
110 (schemaAnnotation.order ?? []).map((key, index) => [key, index] as const),
111 );
112 const orderFallbackOffset = orderedKeys.size;
113
114 return Object.keys(definition.settingsSchema.fields)
115 .map((key, index) => ({ key, index }))
116 .toSorted((left, right) => {
117 return (
118 (orderedKeys.get(left.key) ?? orderFallbackOffset + left.index) -
119 (orderedKeys.get(right.key) ?? orderFallbackOffset + right.index)
120 );
121 })
122 .flatMap(({ key }) => {
123 const fieldSchema = definition.settingsSchema.fields[key]!;
124 const formAnnotation = readProviderSettingsFormAnnotation(fieldSchema);
125 if (formAnnotation.hidden) return [];
126 if (isLocalAcp && key !== "source" && key !== "commandPath") return [];
127
128 const annotatedTitle = readFieldAnnotationString(fieldSchema, "title");
129 const annotatedDescription = readFieldAnnotationString(fieldSchema, "description");
130 return [
131 {
132 key,
133 control: formAnnotation.control ?? "text",
134 label:
135 isLocalAcp && key === "commandPath"
136 ? "Executable"
137 : (annotatedTitle ?? titleizeFieldKey(key)),
138 ...(isLocalAcp && key === "commandPath"
139 ? { description: "Executable name or path on this environment." }
140 : annotatedDescription !== undefined
141 ? { description: annotatedDescription }
142 : {}),
143 ...(isLocalAcp && key === "commandPath"
144 ? { placeholder: "e.g. dsh" }
145 : formAnnotation.placeholder !== undefined
146 ? { placeholder: formAnnotation.placeholder }
147 : {}),
148 clearWhenEmpty: formAnnotation.clearWhenEmpty ?? "omit",
149 ...(isSecretSetting(fieldSchema) ? { secret: true } : {}),
150 ...(acceptsSecretReference(fieldSchema) ? { acceptsSecretReference: true } : {}),
151 ...(formAnnotation.control === "switch"
152 ? { defaultBooleanValue: readFieldBooleanDefault(fieldSchema) }
153 : {}),
154 accepts: fieldAcceptsString(fieldSchema),
155 ...(formAnnotation.control === "select" && formAnnotation.options
156 ? { options: formAnnotation.options }
157 : {}),
158 } satisfies ProviderSettingsFieldModel,
159 ];
160 });
161}
162
163let commandArgumentDraftId = 0;
164/**
165 * Editable rows seeded from a configured value. `seen` is the value the rows
166 * were last reconciled against; `lastPublished` is what the rows last sent to
167 * the owner, so its echo does not reseed rows (and drop their ids) mid-edit.
168 */
169export interface RowsDraft<V, R> {
170 readonly rows: ReadonlyArray<R>;
171 readonly seen: V;
172 readonly lastPublished: V | undefined;
173}
174
175export function seedRowsDraft<V, R>(
176 value: V,
177 toRows: (value: V) => ReadonlyArray<R>,
178): RowsDraft<V, R> {
179 return { rows: toRows(value), seen: value, lastPublished: undefined };
180}
181
182/** Reseeds the rows when the configured value changes from outside this editor. */
183export function reconcileRowsDraft<V, R>(
184 draft: RowsDraft<V, R>,
185 incoming: V,
186 equal: (left: V, right: V) => boolean,
187 toRows: (value: V) => ReadonlyArray<R>,
188): RowsDraft<V, R> {
189 if (draft.seen === incoming || equal(draft.seen, incoming)) return draft;
190 const echo = draft.lastPublished !== undefined && equal(draft.lastPublished, incoming);
191 return { rows: echo ? draft.rows : toRows(incoming), seen: incoming, lastPublished: undefined };
192}
193
194const makeCommandArgumentDraftRow = (value: string) => ({
195 id: `provider-argument-${commandArgumentDraftId++}`,
196 value,
197});
198
199const toCommandArgumentRows = (args: ReadonlyArray<string>) =>
200 args.map(makeCommandArgumentDraftRow);
201
202function commandArgumentsEqual(left: ReadonlyArray<string>, right: ReadonlyArray<string>) {
203 return left.length === right.length && left.every((argument, index) => argument === right[index]);
204}
205
206function configuredCommandArgs(value: ProviderSettingsFormProps["value"]) {
207 const configured =
208 value !== null && typeof value === "object"
209 ? (value as Record<string, unknown>).commandArgs
210 : undefined;
211 return Array.isArray(configured)
212 ? configured.filter((argument): argument is string => typeof argument === "string")
213 : [];
214}
215
216function ProviderCommandArguments({
217 value,
218 onChange,
219 managedReason,
220}: Pick<ProviderSettingsFormProps, "value" | "onChange"> & {
221 readonly managedReason: string | undefined;
222}) {
223 const args = configuredCommandArgs(value);
224 const [storedDraft, setDraft] = useState(() => seedRowsDraft(args, toCommandArgumentRows));
225 const draft = reconcileRowsDraft(storedDraft, args, commandArgumentsEqual, toCommandArgumentRows);
226 if (draft !== storedDraft) setDraft(draft);
227 const rows = draft.rows;
228
229 const updateArguments = (nextRows: typeof rows) => {
230 const next = nextRows.map((row) => row.value);
231 setDraft({ rows: nextRows, seen: draft.seen, lastPublished: next });
232 const config =
233 value !== null && typeof value === "object" ? { ...(value as Record<string, unknown>) } : {};
234 onChange({ ...config, commandArgs: next });
235 };
236
237 return (
238 <SettingsRow
239 title="Arguments"
240 description="One literal argument per row, in launch order."
241 managed={managedReason}
242 control={
243 <Button
244 type="button"
245 size="sm"
246 variant="outline"
247 onClick={() => updateArguments([...rows, makeCommandArgumentDraftRow("")])}
248 >
249 <PlusIcon />
250 Add argument
251 </Button>
252 }
253 >
254 {rows.length > 0 ? (
255 <div className="mt-3 min-w-0 space-y-2 pb-2">
256 {rows.map((argument, index) =>
257 managedReason !== undefined ? (
258 <code key={argument.id} className="block min-w-0 truncate text-xs">
259 {argument.value}
260 </code>
261 ) : (
262 <div key={argument.id} className="flex min-w-0 items-center gap-1.5">
263 <DraftInput
264 size="sm"
265 font="mono"
266 value={argument.value}
267 onCommit={(next) =>
268 updateArguments(
269 rows.map((current) =>
270 current.id === argument.id ? { ...current, value: next } : current,
271 ),
272 )
273 }
274 aria-label={`Argument ${index + 1}`}
275 spellCheck={false}
276 />
277 <Button
278 type="button"
279 size="icon-micro"
280 variant="ghost-destructive"
281 onClick={() =>
282 updateArguments(rows.filter((current) => current.id !== argument.id))
283 }
284 aria-label={`Remove argument ${index + 1}`}
285 >
286 <XIcon />
287 </Button>
288 </div>
289 ),
290 )}
291 </div>
292 ) : null}
293 </SettingsRow>
294 );
295}
296
297function readProviderConfigString(config: unknown, key: string): string {
298 if (config === null || typeof config !== "object") return "";
299 const value = (config as Record<string, unknown>)[key];
300 return typeof value === "string" ? value : "";
301}
302
303function readProviderConfigBoolean(config: unknown, key: string, defaultValue = false): boolean {
304 if (config === null || typeof config !== "object") return defaultValue;
305 const value = (config as Record<string, unknown>)[key];
306 return typeof value === "boolean" ? value : defaultValue;
307}
308
309export function nextProviderConfigWithFieldValue(
310 config: unknown,
311 field: ProviderSettingsFieldModel,
312 value: string | boolean | SecretReference,
313): Record<string, unknown> | undefined {
314 const base: Record<string, unknown> =
315 config !== null && typeof config === "object" ? { ...(config as Record<string, unknown>) } : {};
316
317 if (typeof value === "object") {
318 base[field.key] = value;
319 return base;
320 }
321
322 if (typeof value === "boolean") {
323 const emptyBooleanValue = field.defaultBooleanValue ?? false;
324 if (field.clearWhenEmpty === "omit" && value === emptyBooleanValue) {
325 delete base[field.key];
326 } else {
327 base[field.key] = value;
328 }
329 return Object.keys(base).length > 0 ? base : undefined;
330 }
331
332 const trimmed = value.trim();
333 if (field.clearWhenEmpty === "omit" && trimmed.length === 0) {
334 delete base[field.key];
335 } else if (field.accepts?.(value) !== false) {
336 base[field.key] = value;
337 }
338 return Object.keys(base).length > 0 ? base : undefined;
339}
340
341interface ProviderSettingsFormProps {
342 readonly definition: SettingsFormDefinition;
343 readonly value: unknown;
344 readonly idPrefix: string;
345 readonly variant: "card" | "dialog" | "settings";
346 readonly onChange: (nextConfig: Record<string, unknown> | undefined) => void;
347 /** Config keys an administrator's policy pins, shown inert with the reason. */
348 readonly managed?: { readonly keys: ReadonlySet<string>; readonly reason: string } | undefined;
349 /** Where the secret providers offered for `SettingsSecretValue` fields are configured. */
350 readonly environmentId?: EnvironmentId | undefined;
351}
352
353function SecretSettingsControl({
354 field,
355 value,
356 inputId,
357 descriptionId,
358 environmentId,
359 onChange,
360}: {
361 readonly field: ProviderSettingsFieldModel;
362 readonly value: unknown;
363 readonly inputId: string;
364 readonly descriptionId: string | undefined;
365 readonly environmentId: EnvironmentId;
366 readonly onChange: ProviderSettingsFormProps["onChange"];
367}) {
368 const sources = useSecretSources(environmentId);
369 const saved =
370 value !== null && typeof value === "object"
371 ? (value as Record<string, unknown>)[field.key]
372 : undefined;
373 const reference = isSecretReference(saved) ? saved : undefined;
374 const configured = reference !== undefined || (typeof saved === "string" && saved.length > 0);
375 return (
376 <div className="flex w-full min-w-0 items-start gap-2 @min-[32rem]/settings-row:w-80">
377 <SecretSourceInput
378 id={inputId}
379 aria-describedby={descriptionId}
380 aria-label={field.label}
381 size="sm"
382 className="min-w-0 flex-1"
383 value={reference ?? ""}
384 configured={reference === undefined && configured}
385 sources={sources}
386 environmentId={environmentId}
387 onCommit={(next) => onChange(nextProviderConfigWithFieldValue(value, field, next))}
388 onSecretChange={(secret) =>
389 onChange(nextProviderConfigWithFieldValue(value, field, secret ?? ""))
390 }
391 />
392 {configured ? (
393 <Button
394 type="button"
395 size="icon-sm"
396 variant="ghost-destructive"
397 onClick={() => onChange(nextProviderConfigWithFieldValue(value, field, ""))}
398 aria-label={`Clear ${field.label}`}
399 >
400 <XIcon className="size-3.5" />
401 </Button>
402 ) : null}
403 </div>
404 );
405}
406
407function ProviderSettingsSelect({
408 field,
409 value,
410 inputId,
411 size,
412 className,
413 onChange,
414}: {
415 readonly field: ProviderSettingsFieldModel;
416 readonly value: unknown;
417 readonly inputId: string;
418 readonly size: "sm" | "xs";
419 readonly className?: string | undefined;
420 readonly onChange: ProviderSettingsFormProps["onChange"];
421}) {
422 const options = field.options ?? [];
423 const fallback = options[0]?.value ?? "";
424 const current = readProviderConfigString(value, field.key) || fallback;
425 const label = options.find((option) => option.value === current)?.label ?? current;
426 return (
427 <Select
428 value={current}
429 onValueChange={(next) => {
430 if (typeof next !== "string") return;
431 onChange(nextProviderConfigWithFieldValue(value, field, next === fallback ? "" : next));
432 }}
433 >
434 <SelectTrigger id={inputId} size={size} className={className} aria-label={field.label}>
435 <SelectValue>{label}</SelectValue>
436 </SelectTrigger>
437 <SelectPopup align="start" alignItemWithTrigger={false}>
438 {options.map((option) => (
439 <SelectItem key={option.value} value={option.value}>
440 {option.label}
441 </SelectItem>
442 ))}
443 </SelectPopup>
444 </Select>
445 );
446}
447
448function ProviderSettingsTextInput({
449 field,
450 value,
451 inputId,
452 descriptionId,
453 onChange,
454}: {
455 readonly field: ProviderSettingsFieldModel;
456 readonly value: unknown;
457 readonly inputId: string;
458 readonly descriptionId: string | undefined;
459 readonly onChange: ProviderSettingsFormProps["onChange"];
460}) {
461 const [rejected, setRejected] = useState<string | null>(null);
462 return (
463 <div className="flex w-full min-w-0 flex-col gap-1 @min-[32rem]/settings-row:w-56">
464 <DraftInput
465 id={inputId}
466 aria-describedby={descriptionId}
467 size="sm"
468 className="w-full max-w-full"
469 type={field.control === "password" ? "password" : undefined}
470 autoComplete={field.control === "password" ? "off" : undefined}
471 value={rejected ?? readProviderConfigString(value, field.key)}
472 placeholder={field.placeholder}
473 onCommit={(next) => {
474 const clears = field.clearWhenEmpty === "omit" && next.trim().length === 0;
475 if (!clears && field.accepts?.(next) === false) {
476 setRejected(next);
477 return;
478 }
479 setRejected(null);
480 onChange(nextProviderConfigWithFieldValue(value, field, next));
481 }}
482 aria-invalid={rejected !== null || undefined}
483 spellCheck={false}
484 />
485 {rejected !== null ? (
486 <p className="text-2xs text-destructive">Enter a valid value. It was not saved.</p>
487 ) : null}
488 </div>
489 );
490}
491
492function FieldFrame(props: {
493 readonly variant: ProviderSettingsFormProps["variant"];
494 readonly children: ReactNode;
495}) {
496 if (props.variant === "card") {
497 return <div>{props.children}</div>;
498 }
499 return <div className="grid gap-1.5">{props.children}</div>;
500}
501
502interface ProviderSettingsFieldRowProps {
503 readonly field: ProviderSettingsFieldModel;
504 readonly value: unknown;
505 readonly idPrefix: string;
506 readonly variant: ProviderSettingsFormProps["variant"];
507 readonly onChange: ProviderSettingsFormProps["onChange"];
508 readonly managedReason?: string | undefined;
509 readonly environmentId: EnvironmentId | undefined;
510}
511
512function ProviderSettingsFieldRow({
513 field,
514 value,
515 idPrefix,
516 variant,
517 onChange,
518 managedReason,
519 environmentId,
520}: ProviderSettingsFieldRowProps) {
521 const inputId = `${idPrefix}-${field.key}`;
522 const descriptionClassName =
523 variant === "dialog"
524 ? "text-2xs text-muted-foreground"
525 : "mt-1 block text-xs text-muted-foreground";
526 const label = <span className="text-xs font-medium text-foreground">{field.label}</span>;
527 const description = field.description ? (
528 <span className={descriptionClassName}>{field.description}</span>
529 ) : null;
530
531 if (variant === "settings") {
532 const descriptionId = field.description ? `${inputId}-description` : undefined;
533 const secretConfigured =
534 field.secret === true && readProviderConfigString(value, field.key).length > 0;
535 const control =
536 field.control === "switch" ? (
537 <Switch
538 checked={readProviderConfigBoolean(value, field.key, field.defaultBooleanValue)}
539 onCheckedChange={(checked) =>
540 onChange(nextProviderConfigWithFieldValue(value, field, Boolean(checked)))
541 }
542 aria-label={field.label}
543 aria-describedby={descriptionId}
544 />
545 ) : field.control === "select" ? (
546 <ProviderSettingsSelect
547 field={field}
548 value={value}
549 inputId={inputId}
550 size="sm"
551 className="w-full max-w-full @min-[32rem]/settings-row:w-56"
552 onChange={onChange}
553 />
554 ) : field.control === "textarea" ? (
555 <Textarea
556 id={inputId}
557 aria-describedby={descriptionId}
558 className="w-full max-w-full @min-[32rem]/settings-row:w-[min(24rem,50cqw)]"
559 value={readProviderConfigString(value, field.key)}
560 onChange={(event) =>
561 onChange(nextProviderConfigWithFieldValue(value, field, event.target.value))
562 }
563 placeholder={field.placeholder}
564 spellCheck={false}
565 />
566 ) : field.acceptsSecretReference && environmentId !== undefined ? (
567 <SecretSettingsControl
568 field={field}
569 value={value}
570 inputId={inputId}
571 descriptionId={descriptionId}
572 environmentId={environmentId}
573 onChange={onChange}
574 />
575 ) : field.secret ? (
576 <div className="flex w-full min-w-0 items-center gap-2 @min-[32rem]/settings-row:w-56">
577 <SecretInput
578 id={inputId}
579 aria-describedby={descriptionId}
580 size="sm"
581 className="min-w-0 flex-1"
582 configured={secretConfigured}
583 value=""
584 onCommit={(next) => onChange(nextProviderConfigWithFieldValue(value, field, next))}
585 />
586 {secretConfigured ? (
587 <Button
588 type="button"
589 size="icon-sm"
590 variant="ghost-destructive"
591 onClick={() => onChange(nextProviderConfigWithFieldValue(value, field, ""))}
592 aria-label={`Clear ${field.label}`}
593 >
594 <XIcon className="size-3.5" />
595 </Button>
596 ) : null}
597 </div>
598 ) : (
599 <ProviderSettingsTextInput
600 field={field}
601 value={value}
602 inputId={inputId}
603 descriptionId={descriptionId}
604 onChange={onChange}
605 />
606 );
607
608 return (
609 <SettingsRow
610 title={
611 field.control === "switch" ? field.label : <label htmlFor={inputId}>{field.label}</label>
612 }
613 description={
614 field.description ? <span id={descriptionId}>{field.description}</span> : undefined
615 }
616 control={control}
617 managed={managedReason}
618 />
619 );
620 }
621
622 if (field.control === "switch") {
623 return (
624 <FieldFrame variant={variant}>
625 <div className="flex items-center justify-between gap-3">
626 <div className="min-w-0">
627 {label}
628 {description}
629 </div>
630 <Switch
631 checked={readProviderConfigBoolean(value, field.key, field.defaultBooleanValue)}
632 onCheckedChange={(checked) =>
633 onChange(nextProviderConfigWithFieldValue(value, field, Boolean(checked)))
634 }
635 aria-label={field.label}
636 />
637 </div>
638 </FieldFrame>
639 );
640 }
641
642 if (field.control === "select") {
643 return (
644 <FieldFrame variant={variant}>
645 <label htmlFor={inputId} className={cn(variant === "card" && "block")}>
646 {label}
647 <ProviderSettingsSelect
648 field={field}
649 value={value}
650 inputId={inputId}
651 size="sm"
652 className={cn("w-full", variant === "card" && "mt-1.5")}
653 onChange={onChange}
654 />
655 {description}
656 </label>
657 </FieldFrame>
658 );
659 }
660
661 if (field.control === "textarea") {
662 return (
663 <FieldFrame variant={variant}>
664 <label htmlFor={inputId} className={cn(variant === "card" && "block")}>
665 {label}
666 <Textarea
667 id={inputId}
668 className={cn(variant === "card" && "mt-1.5")}
669 value={readProviderConfigString(value, field.key)}
670 onChange={(event) =>
671 onChange(nextProviderConfigWithFieldValue(value, field, event.target.value))
672 }
673 placeholder={field.placeholder}
674 spellCheck={false}
675 />
676 {description}
677 </label>
678 </FieldFrame>
679 );
680 }
681
682 const type = field.control === "password" ? "password" : undefined;
683 const BufferedInput = field.secret ? SecretInput : DraftInput;
684 const ControlledInput = field.secret ? SecretInput : Input;
685 return (
686 <FieldFrame variant={variant}>
687 <label htmlFor={inputId} className={cn(variant === "card" && "block")}>
688 {label}
689 {variant === "card" ? (
690 <BufferedInput
691 id={inputId}
692 size="sm"
693 className="mt-1.5"
694 type={field.secret ? undefined : type}
695 autoComplete={!field.secret && field.control === "password" ? "off" : undefined}
696 value={readProviderConfigString(value, field.key)}
697 onCommit={(next) => onChange(nextProviderConfigWithFieldValue(value, field, next))}
698 placeholder={field.placeholder}
699 spellCheck={false}
700 />
701 ) : (
702 <ControlledInput
703 id={inputId}
704 type={field.secret ? undefined : type}
705 autoComplete={!field.secret && field.control === "password" ? "off" : undefined}
706 value={readProviderConfigString(value, field.key)}
707 onChange={(event) =>
708 onChange(nextProviderConfigWithFieldValue(value, field, event.target.value))
709 }
710 placeholder={field.placeholder}
711 spellCheck={false}
712 />
713 )}
714 {description}
715 </label>
716 </FieldFrame>
717 );
718}
719
720export function ProviderSettingsForm({
721 definition,
722 value,
723 idPrefix,
724 variant,
725 onChange,
726 managed,
727 environmentId,
728}: ProviderSettingsFormProps) {
729 const fields = deriveProviderSettingsFields(definition, value);
730 const isLocalAcp =
731 definition.driverKind === "acpRegistry" &&
732 readProviderConfigString(value, "source") === "local";
733
734 if (fields.length === 0) {
735 return null;
736 }
737
738 return (
739 <>
740 {fields.map((field) => (
741 <ProviderSettingsFieldRow
742 key={field.key}
743 field={field}
744 value={value}
745 idPrefix={idPrefix}
746 variant={variant}
747 onChange={onChange}
748 managedReason={managed?.keys.has(field.key) ? managed.reason : undefined}
749 environmentId={environmentId}
750 />
751 ))}
752 {isLocalAcp ? (
753 <ProviderCommandArguments
754 value={value}
755 onChange={onChange}
756 managedReason={managed?.keys.has("commandArgs") ? managed.reason : undefined}
757 />
758 ) : null}
759 </>
760 );
761}