Skip to content
Source
Sign inSign up

SecretProviderDialog.tsx

155 lines5.2 KB

1import type { EnvironmentId, SecretProviderConfig, SecretProviderId } from "@t3tools/contracts";
2import * as Result from "effect/Result";
3import { useState } from "react";
4​
5import { useUpdateEnvironmentSettings } from "../../hooks/useSettings";
6import { Button } from "../ui/button";
7import {
8 Dialog,
9 DialogDescription,
10 DialogFooter,
11 DialogHeader,
12 DialogPanel,
13 DialogPopup,
14 DialogTitle,
15} from "../ui/dialog";
16import { Input } from "../ui/input";
17import { Label } from "../ui/label";
18import {
19 draftFromSecretProvider,
20 secretProviderFromDraft,
21 type SecretProviderDraft,
22} from "./SecretProviderDialog.logic";
23​
24/** Mounted per open, so each open starts from a fresh draft. */
25export function SecretProviderDialog({
26 onClose,
27 environmentId,
28 environmentLabel,
29 existingIds,
30 existing,
31}: {
32 readonly onClose: () => void;
33 readonly environmentId: EnvironmentId;
34 readonly environmentLabel: string;
35 readonly existingIds: ReadonlySet<string>;
36 readonly existing?: { readonly id: SecretProviderId; readonly config: SecretProviderConfig };
37}) {
38 const updateSettings = useUpdateEnvironmentSettings(environmentId);
39 const [draft, setDraft] = useState<SecretProviderDraft>(
40 existing ? draftFromSecretProvider(existing.config) : { name: "", account: "" },
41 );
42 const [submitted, setSubmitted] = useState(false);
43 const result = secretProviderFromDraft(
44 draft,
45 existingIds,
46 existing ? { mode: "edit", id: existing.id } : { mode: "add" },
47 );
48 const issues = Result.isFailure(result) ? result.failure : {};
49 const nameError = submitted ? issues.name : undefined;
50 const accountError = submitted ? issues.account : undefined;
51​
52 const update = (patch: Partial<SecretProviderDraft>) => setDraft({ ...draft, ...patch });
53 const save = () => {
54 setSubmitted(true);
55 if (Result.isFailure(result)) return;
56 updateSettings({ secretProviders: { [result.success.id]: result.success.config } });
57 onClose();
58 };
59​
60 return (
61 <Dialog
62 open
63 onOpenChange={(next) => {
64 if (!next) onClose();
65 }}
66 >
67 <DialogPopup className="max-w-md">
68 <DialogHeader>
69 <DialogTitle>
70 {existing ? `Edit ${existing.config.label}` : "Add a 1Password provider"}
71 </DialogTitle>
72 <DialogDescription>
73 Provider variables on {environmentLabel} can read values from it by name. Values are
74 read with the op CLI on that server when a provider starts.
75 </DialogDescription>
76 </DialogHeader>
77 <DialogPanel>
78 <form
79 id="secret-provider-form"
80 className="grid gap-4"
81 onSubmit={(event) => {
82 event.preventDefault();
83 save();
84 }}
85 >
86 <div className="grid gap-1.5">
87 <Label htmlFor="secret-provider-name">Name</Label>
88 <Input
89 id="secret-provider-name"
90 placeholder="Personal"
91 value={draft.name}
92 onChange={(event) => update({ name: event.target.value })}
93 maxLength={64}
94 aria-invalid={nameError !== undefined}
95 aria-describedby={
96 nameError !== undefined ? "secret-provider-name-error" : undefined
97 }
98 autoFocus
99 />
100 {nameError !== undefined ? (
101 <p
102 id="secret-provider-name-error"
103 role="alert"
104 className="text-xs text-destructive"
105 >
106 {nameError}
107 </p>
108 ) : null}
109 </div>
110 <div className="grid gap-1.5">
111 <Label htmlFor="secret-provider-account">Account</Label>
112 <Input
113 id="secret-provider-account"
114 placeholder="my.1password.com"
115 value={draft.account}
116 onChange={(event) => update({ account: event.target.value })}
117 maxLength={253}
118 spellCheck={false}
119 aria-invalid={accountError !== undefined}
120 aria-describedby={
121 accountError !== undefined ? "secret-provider-account-error" : undefined
122 }
123 />
124 {accountError !== undefined ? (
125 <p
126 id="secret-provider-account-error"
127 role="alert"
128 className="text-xs text-destructive"
129 >
130 {accountError}
131 </p>
132 ) : null}
133 <p className="text-xs text-muted-foreground">
134 The account shorthand, sign-in address without https://, or ID that op account list
135 shows.
136 </p>
137 </div>
138 </form>
139 </DialogPanel>
140 <DialogFooter variant="bare">
141 <Button variant="outline" onClick={onClose}>
142 Cancel
143 </Button>
144 <Button
145 type="submit"
146 form="secret-provider-form"
147 disabled={submitted && Result.isFailure(result)}
148 >
149 {existing ? "Save changes" : "Add provider"}
150 </Button>
151 </DialogFooter>
152 </DialogPopup>
153 </Dialog>
154 );