Skip to content

Commit ca2b186

Browse files
Add more guarantees around a secret existing
1 parent ca2b997 commit ca2b186

File tree

3 files changed

+135
-75
lines changed

3 files changed

+135
-75
lines changed

packages/web/src/app/[domain]/components/connectionCreationForms/secretCombobox.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "@/components/ui/command"
1212
import { Button } from "@/components/ui/button";
1313
import { cn, isServiceError } from "@/lib/utils";
14-
import { ChevronsUpDown, Check, PlusCircleIcon, Loader2, Eye, EyeOff } from "lucide-react";
14+
import { ChevronsUpDown, Check, PlusCircleIcon, Loader2, Eye, EyeOff, TriangleAlert } from "lucide-react";
1515
import { useCallback, useMemo, useState } from "react";
1616
import { Separator } from "@/components/ui/separator";
1717
import { useQuery } from "@tanstack/react-query";
@@ -28,6 +28,8 @@ import { useToast } from "@/components/hooks/use-toast";
2828
import Image from "next/image";
2929
import githubPatCreation from "@/public/github_pat_creation.png"
3030
import { CodeHostType } from "@/lib/utils";
31+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
32+
import { isDefined } from '@/lib/utils'
3133

3234
interface SecretComboBoxProps {
3335
isDisabled: boolean;
@@ -56,21 +58,49 @@ export const SecretCombobox = ({
5658
refetch();
5759
}, [onSecretChange, refetch]);
5860

61+
const isSecretNotFoundWarningVisible = useMemo(() => {
62+
if (!isDefined(secretKey)) {
63+
return false;
64+
}
65+
if (isServiceError(secrets)) {
66+
return false;
67+
}
68+
return !secrets?.some(({ key }) => key === secretKey);
69+
}, [secretKey, secrets]);
70+
5971
return (
6072
<>
6173
<Popover>
6274
<PopoverTrigger asChild>
75+
6376
<Button
6477
variant="outline"
6578
role="combobox"
6679
className={cn(
67-
"w-[300px] justify-between overflow-hidden",
80+
"w-[300px] overflow-hidden",
6881
!secretKey && "text-muted-foreground"
6982
)}
7083
disabled={isDisabled}
7184
>
72-
<span className="truncate">{secretKey ? secretKey : "Select secret"}</span>
73-
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
85+
{isSecretNotFoundWarningVisible && (
86+
<TooltipProvider>
87+
88+
<Tooltip
89+
delayDuration={100}
90+
>
91+
<TooltipTrigger
92+
onClick={(e) => e.preventDefault()}
93+
>
94+
<TriangleAlert className="h-4 w-4 text-yellow-700 dark:text-yellow-400" />
95+
</TooltipTrigger>
96+
<TooltipContent>
97+
<p>The secret you selected does not exist.</p>
98+
</TooltipContent>
99+
</Tooltip>
100+
</TooltipProvider>
101+
)}
102+
<span className="truncate">{isDefined(secretKey) ? secretKey : "Select secret"}</span>
103+
<ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 opacity-50" />
74104
</Button>
75105
</PopoverTrigger>
76106
<PopoverContent className="p-0.5">

packages/web/src/app/[domain]/components/connectionCreationForms/sharedConnectionCreationForm.tsx

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
'use client';
33

4-
import { createConnection } from "@/actions";
4+
import { checkIfSecretExists, createConnection } from "@/actions";
55
import { ConnectionIcon } from "@/app/[domain]/connections/components/connectionIcon";
66
import { createZodConnectionConfigValidator } from "@/app/[domain]/connections/utils";
77
import { useToast } from "@/components/hooks/use-toast";
@@ -56,6 +56,13 @@ export default function SharedConnectionCreationForm<T>({
5656
return z.object({
5757
name: z.string().min(1),
5858
config: createZodConnectionConfigValidator(schema),
59+
secretKey: z.string().optional().refine(async (secretKey) => {
60+
if (!secretKey) {
61+
return true;
62+
}
63+
64+
return checkIfSecretExists(secretKey, domain);
65+
}, { message: "Secret not found" }),
5966
});
6067
}, [schema]);
6168

@@ -86,19 +93,19 @@ export default function SharedConnectionCreationForm<T>({
8693
if (isValid) {
8794
const configJson = JSON.parse(value);
8895
if (configJson.token?.secret !== undefined) {
89-
setSecretKey(configJson.token.secret);
96+
form.setValue("secretKey", configJson.token.secret);
9097
} else {
91-
setSecretKey(undefined);
98+
form.setValue("secretKey", undefined);
9299
}
93100
}
94101
}, [form]);
95102

103+
// Run onConfigChange on mount to set the initial secret key
96104
useEffect(() => {
97105
onConfigChange(defaultValues.config);
98106
}, [defaultValues, onConfigChange]);
99107

100108
const [isSecretsDisabled, setIsSecretsDisabled] = useState(false);
101-
const [secretKey, setSecretKey] = useState<string | undefined>(undefined);
102109

103110
return (
104111
<div className={cn("flex flex-col max-w-3xl mx-auto bg-background border rounded-lg p-6", className)}>
@@ -133,38 +140,47 @@ export default function SharedConnectionCreationForm<T>({
133140
)}
134141
/>
135142
{isAuthSupportedForCodeHost(type) && (
136-
<div className="flex flex-col gap-2">
137-
<FormLabel>Secret (optional)</FormLabel>
138-
<FormDescription>{strings.createSecretDescription}</FormDescription>
139-
<SecretCombobox
140-
isDisabled={isSecretsDisabled}
141-
secretKey={secretKey}
142-
codeHostType={type}
143-
onSecretChange={(secretKey) => {
144-
const view = editorRef.current?.view;
145-
if (!view) {
146-
return;
147-
}
148-
149-
onQuickAction(
150-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
151-
(previous: any) => {
152-
return {
153-
...previous,
154-
token: {
155-
secret: secretKey,
143+
<FormField
144+
control={form.control}
145+
name="secretKey"
146+
render={({ field: { value } }) => (
147+
<FormItem>
148+
<FormLabel>Secret (optional)</FormLabel>
149+
<FormDescription>{strings.createSecretDescription}</FormDescription>
150+
<FormControl>
151+
<SecretCombobox
152+
isDisabled={isSecretsDisabled}
153+
secretKey={value}
154+
codeHostType={type}
155+
onSecretChange={(secretKey) => {
156+
const view = editorRef.current?.view;
157+
if (!view) {
158+
return;
156159
}
157-
}
158-
},
159-
form.getValues("config"),
160-
view,
161-
{
162-
focusEditor: false
163-
}
164-
);
165-
}}
166-
/>
167-
</div>
160+
161+
onQuickAction(
162+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
163+
(previous: any) => {
164+
return {
165+
...previous,
166+
token: {
167+
secret: secretKey,
168+
}
169+
}
170+
},
171+
form.getValues("config"),
172+
view,
173+
{
174+
focusEditor: false
175+
}
176+
);
177+
}}
178+
/>
179+
</FormControl>
180+
<FormMessage />
181+
</FormItem>
182+
)}
183+
/>
168184
)}
169185
<FormField
170186
control={form.control}

packages/web/src/app/[domain]/connections/[id]/components/configSetting.tsx

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { githubQuickActions, gitlabQuickActions, giteaQuickActions, gerritQuickA
1717
import { Schema } from "ajv";
1818
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
1919
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
20-
import { updateConnectionConfigAndScheduleSync } from "@/actions";
20+
import { checkIfSecretExists, updateConnectionConfigAndScheduleSync } from "@/actions";
2121
import { useToast } from "@/components/hooks/use-toast";
2222
import { isServiceError, CodeHostType, isAuthSupportedForCodeHost } from "@/lib/utils";
2323
import { useRouter } from "next/navigation";
@@ -93,11 +93,17 @@ function ConfigSettingInternal<T>({
9393
const domain = useDomain();
9494
const editorRef = useRef<ReactCodeMirrorRef>(null);
9595
const [isSecretsDisabled, setIsSecretsDisabled] = useState(false);
96-
const [secretKey, setSecretKey] = useState<string | undefined>(undefined);
9796

9897
const formSchema = useMemo(() => {
9998
return z.object({
10099
config: createZodConnectionConfigValidator(schema),
100+
secretKey: z.string().optional().refine(async (secretKey) => {
101+
if (!secretKey) {
102+
return true;
103+
}
104+
105+
return checkIfSecretExists(secretKey, domain);
106+
}, { message: "Secret not found" })
101107
});
102108
}, [schema]);
103109

@@ -137,9 +143,9 @@ function ConfigSettingInternal<T>({
137143
if (isValid) {
138144
const configJson = JSON.parse(value);
139145
if (configJson.token?.secret !== undefined) {
140-
setSecretKey(configJson.token.secret);
146+
form.setValue("secretKey", configJson.token.secret);
141147
} else {
142-
setSecretKey(undefined);
148+
form.setValue("secretKey", undefined);
143149
}
144150
}
145151
}, [form]);
@@ -166,39 +172,47 @@ function ConfigSettingInternal<T>({
166172
className="flex flex-col gap-6"
167173
>
168174
{isAuthSupportedForCodeHost(type) && (
169-
<div className="flex flex-col gap-2">
170-
<FormLabel>Secret (optional)</FormLabel>
171-
<FormDescription>{strings.createSecretDescription}</FormDescription>
172-
<SecretCombobox
173-
isDisabled={isSecretsDisabled}
174-
secretKey={secretKey}
175-
codeHostType={type}
176-
onSecretChange={(secretKey) => {
177-
const view = editorRef.current?.view;
178-
console.log(editorRef.current);
179-
if (!view) {
180-
return;
181-
}
182-
183-
onQuickAction(
184-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
185-
(previous: any) => {
186-
return {
187-
...previous,
188-
token: {
189-
secret: secretKey,
190-
}
175+
<FormField
176+
control={form.control}
177+
name="secretKey"
178+
render={({ field: { value } }) => (
179+
<FormItem>
180+
<FormLabel>Secret (optional)</FormLabel>
181+
<FormDescription>{strings.createSecretDescription}</FormDescription>
182+
<FormControl>
183+
<SecretCombobox
184+
isDisabled={isSecretsDisabled}
185+
secretKey={value}
186+
codeHostType={type}
187+
onSecretChange={(secretKey) => {
188+
const view = editorRef.current?.view;
189+
if (!view) {
190+
return;
191191
}
192-
},
193-
form.getValues("config"),
194-
view,
195-
{
196-
focusEditor: false
197-
}
198-
);
199-
}}
200-
/>
201-
</div>
192+
193+
onQuickAction(
194+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
195+
(previous: any) => {
196+
return {
197+
...previous,
198+
token: {
199+
secret: secretKey,
200+
}
201+
}
202+
},
203+
form.getValues("config"),
204+
view,
205+
{
206+
focusEditor: false
207+
}
208+
);
209+
}}
210+
/>
211+
</FormControl>
212+
<FormMessage />
213+
</FormItem>
214+
)}
215+
/>
202216
)}
203217
<FormField
204218
control={form.control}

0 commit comments

Comments
 (0)