1import type { EnvironmentId } from "@t3tools/contracts";
2import type { SourceControlClientDefinition } from "@t3tools/client-runtime/source-control-clients";
3import { useState } from "react";
4
5import { useEnvironmentSettings } from "../../hooks/useSettings";
6import { serverEnvironment } from "../../state/server";
7import { useAtomCommand } from "../../state/use-atom-command";
8import { Button } from "../ui/button";
9import { ProviderSettingsForm } from "./ProviderSettingsForm";
10
11/**
12 * One host's settings, drawn from its definition's settings schema and saved under
13 * `sourceControlHosts[kind]`. Secret fields are write-only: the server keeps them in its secret
14 * store and only reports whether each one is set.
15 */
16export function SourceControlHostSettings({
17 environmentId,
18 definition,
19 onSaved,
20}: {
21 readonly environmentId: EnvironmentId;
22 readonly definition: SourceControlClientDefinition & {
23 readonly settings: NonNullable<SourceControlClientDefinition["settings"]>;
24 };
25 readonly onSaved: () => void;
26}) {
27 const saved = useEnvironmentSettings(
28 environmentId,
29 (settings) => settings.sourceControlHosts[definition.kind],
30 );
31 const updateSettings = useAtomCommand(serverEnvironment.updateSettings, {
32 label: `save ${definition.label} settings`,
33 });
34 const [saving, setSaving] = useState(false);
35 const formDefinition = { settingsSchema: definition.settings };
36 const hasSaved = Object.values(saved ?? {}).some((value) => value !== "");
37
38 const save = async (fields: Record<string, unknown>) => {
39 setSaving(true);
40 try {
41 const result = await updateSettings({
42 environmentId,
43 input: { patch: { sourceControlHosts: { [definition.kind]: fields } } },
44 });
45 if (result._tag === "Success") onSaved();
46 } finally {
47 setSaving(false);
48 }
49 };
50
51 return (
52 // Locked while saving, so an edit made mid-request is not overwritten by the saved value.
53 <fieldset disabled={saving} className="contents">
54 <ProviderSettingsForm
55 definition={formDefinition}
56 value={saved ?? {}}
57 idPrefix={`source-control-${definition.kind}-${environmentId}`}
58 variant="settings"
59 onChange={(next) => {
60 const fields = next ?? {};
61 // Only what changed: each saved secret travels as its redaction marker anyway.
62 const changed = Object.fromEntries(
63 Object.entries(fields).filter(([key, value]) => saved?.[key] !== value),
64 );
65 const cleared = Object.keys(saved ?? {}).filter((key) => !(key in fields));
66 void save({ ...changed, ...Object.fromEntries(cleared.map((key) => [key, ""])) });
67 }}
68 />
69 {hasSaved ? (
70 <div className="flex justify-end pt-2">
71 <Button
72 size="xs"
73 variant="outline"
74 onClick={() =>
75 void save(Object.fromEntries(Object.keys(saved ?? {}).map((key) => [key, ""])))
76 }
77 >
78 Remove
79 </Button>
80 </div>
81 ) : null}
82 </fieldset>
83 );
84}