Skip to content

Commit 83ab0a0

Browse files
/settings/secrets page (#217)
1 parent 5006a93 commit 83ab0a0

File tree

15 files changed

+607
-577
lines changed

15 files changed

+607
-577
lines changed
188 KB
Loading
343 KB
Loading
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client';
2+
3+
import { Button } from "@/components/ui/button";
4+
import useCaptureEvent from "@/hooks/useCaptureEvent";
5+
import { cn } from "@/lib/utils";
6+
import Image from "next/image";
7+
8+
interface CodeHostIconButton {
9+
name: string;
10+
logo: { src: string, className?: string };
11+
onClick: () => void;
12+
}
13+
14+
export const CodeHostIconButton = ({
15+
name,
16+
logo,
17+
onClick,
18+
}: CodeHostIconButton) => {
19+
const captureEvent = useCaptureEvent();
20+
return (
21+
<Button
22+
className="flex flex-col items-center justify-center p-4 w-24 h-24 cursor-pointer gap-2"
23+
variant="outline"
24+
onClick={() => {
25+
captureEvent('wa_connect_code_host_button_pressed', {
26+
name,
27+
})
28+
onClick();
29+
}}
30+
>
31+
<Image src={logo.src} alt={name} className={cn("w-8 h-8", logo.className)} />
32+
<p className="text-sm font-medium">{name}</p>
33+
</Button>
34+
)
35+
}
Lines changed: 11 additions & 274 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,26 @@
11
'use client';
22

3-
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
3+
import { getSecrets } from "@/actions";
4+
import { Button } from "@/components/ui/button";
45
import {
56
Command,
67
CommandEmpty,
78
CommandGroup,
89
CommandInput,
910
CommandItem,
1011
CommandList,
11-
} from "@/components/ui/command"
12-
import { Button } from "@/components/ui/button";
13-
import { cn, isServiceError, unwrapServiceError } from "@/lib/utils";
14-
import { ChevronsUpDown, Check, PlusCircleIcon, Loader2, Eye, EyeOff, TriangleAlert } from "lucide-react";
15-
import { useCallback, useMemo, useState } from "react";
12+
} from "@/components/ui/command";
13+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
1614
import { Separator } from "@/components/ui/separator";
17-
import { useQuery } from "@tanstack/react-query";
18-
import { checkIfSecretExists, createSecret, getSecrets } from "@/actions";
19-
import { useDomain } from "@/hooks/useDomain";
20-
import { Dialog, DialogTitle, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog";
21-
import Link from "next/link";
22-
import { Form, FormLabel, FormControl, FormDescription, FormItem, FormField, FormMessage } from "@/components/ui/form";
23-
import { Input } from "@/components/ui/input";
24-
import { z } from "zod";
25-
import { zodResolver } from "@hookform/resolvers/zod";
26-
import { useForm } from "react-hook-form";
27-
import { useToast } from "@/components/hooks/use-toast";
28-
import Image from "next/image";
29-
import githubPatCreation from "@/public/github_pat_creation.png"
30-
import { CodeHostType } from "@/lib/utils";
3115
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
32-
import { isDefined } from '@/lib/utils'
3316
import useCaptureEvent from "@/hooks/useCaptureEvent";
17+
import { useDomain } from "@/hooks/useDomain";
18+
import { cn, CodeHostType, isDefined, isServiceError, unwrapServiceError } from "@/lib/utils";
19+
import { useQuery } from "@tanstack/react-query";
20+
import { Check, ChevronsUpDown, Loader2, PlusCircleIcon, TriangleAlert } from "lucide-react";
21+
import { useCallback, useState } from "react";
22+
import { ImportSecretDialog } from "../importSecretDialog";
23+
3424
interface SecretComboBoxProps {
3525
isDisabled: boolean;
3626
codeHostType: CodeHostType;
@@ -171,256 +161,3 @@ export const SecretCombobox = ({
171161
</>
172162
)
173163
}
174-
175-
interface ImportSecretDialogProps {
176-
open: boolean;
177-
onOpenChange: (open: boolean) => void;
178-
onSecretCreated: (key: string) => void;
179-
codeHostType: CodeHostType;
180-
}
181-
182-
183-
const ImportSecretDialog = ({ open, onOpenChange, onSecretCreated, codeHostType }: ImportSecretDialogProps) => {
184-
const [showValue, setShowValue] = useState(false);
185-
const domain = useDomain();
186-
const { toast } = useToast();
187-
const captureEvent = useCaptureEvent();
188-
189-
const formSchema = z.object({
190-
key: z.string().min(1).refine(async (key) => {
191-
const doesSecretExist = await checkIfSecretExists(key, domain);
192-
if(!isServiceError(doesSecretExist)) {
193-
captureEvent('wa_secret_combobox_import_secret_fail', {
194-
type: codeHostType,
195-
error: "A secret with this key already exists.",
196-
});
197-
}
198-
return isServiceError(doesSecretExist) || !doesSecretExist;
199-
}, "A secret with this key already exists."),
200-
value: z.string().min(1),
201-
});
202-
203-
const form = useForm<z.infer<typeof formSchema>>({
204-
resolver: zodResolver(formSchema),
205-
defaultValues: {
206-
key: "",
207-
value: "",
208-
},
209-
});
210-
const { isSubmitting } = form.formState;
211-
212-
const onSubmit = useCallback(async (data: z.infer<typeof formSchema>) => {
213-
const response = await createSecret(data.key, data.value, domain);
214-
if (isServiceError(response)) {
215-
toast({
216-
description: `❌ Failed to create secret`
217-
});
218-
captureEvent('wa_secret_combobox_import_secret_fail', {
219-
type: codeHostType,
220-
error: response.message,
221-
});
222-
} else {
223-
toast({
224-
description: `✅ Secret created successfully!`
225-
});
226-
captureEvent('wa_secret_combobox_import_secret_success', {
227-
type: codeHostType,
228-
});
229-
form.reset();
230-
onOpenChange(false);
231-
onSecretCreated(data.key);
232-
}
233-
}, [domain, toast, onOpenChange, onSecretCreated, form, codeHostType, captureEvent]);
234-
235-
const codeHostSpecificStep = useMemo(() => {
236-
switch (codeHostType) {
237-
case 'github':
238-
return <GitHubPATCreationStep step={1} />;
239-
case 'gitlab':
240-
return <GitLabPATCreationStep step={1} />;
241-
case 'gitea':
242-
return <GiteaPATCreationStep step={1} />;
243-
case 'gerrit':
244-
return null;
245-
}
246-
}, [codeHostType]);
247-
248-
249-
return (
250-
<Dialog
251-
open={open}
252-
onOpenChange={onOpenChange}
253-
>
254-
<DialogContent
255-
className="p-16 max-w-[90vw] sm:max-w-2xl max-h-[80vh] overflow-scroll rounded-lg"
256-
>
257-
<DialogHeader>
258-
<DialogTitle className="text-2xl font-semibold">Import a secret</DialogTitle>
259-
<DialogDescription>
260-
Secrets are used to authenticate with a code host. They are encrypted at rest using <Link href="https://en.wikipedia.org/wiki/Advanced_Encryption_Standard" target="_blank" className="underline">AES-256-CBC</Link>.
261-
Checkout our <Link href="https://sourcebot.dev/security" target="_blank" className="underline">security docs</Link> for more information.
262-
</DialogDescription>
263-
</DialogHeader>
264-
265-
<Form
266-
{...form}
267-
>
268-
<form
269-
className="space-y-4 flex flex-col mt-4 gap-4"
270-
onSubmit={(event) => {
271-
event.stopPropagation();
272-
form.handleSubmit(onSubmit)(event);
273-
}}
274-
>
275-
{codeHostSpecificStep}
276-
277-
<SecretCreationStep
278-
step={2}
279-
title="Import the secret"
280-
description="Copy the generated token and paste it below."
281-
>
282-
<FormField
283-
control={form.control}
284-
name="value"
285-
render={({ field }) => (
286-
<FormItem>
287-
<FormLabel>Value</FormLabel>
288-
<FormControl>
289-
<div className="relative">
290-
<Input
291-
{...field}
292-
type={showValue ? "text" : "password"}
293-
placeholder="Enter your secret value"
294-
/>
295-
<Button
296-
type="button"
297-
variant="ghost"
298-
size="sm"
299-
className="absolute right-2 top-1/2 -translate-y-1/2"
300-
onClick={() => setShowValue(!showValue)}
301-
>
302-
{showValue ? (
303-
<EyeOff className="h-4 w-4" />
304-
) : (
305-
<Eye className="h-4 w-4" />
306-
)}
307-
</Button>
308-
</div>
309-
</FormControl>
310-
<FormDescription>
311-
The secret value to store securely.
312-
</FormDescription>
313-
<FormMessage />
314-
</FormItem>
315-
)}
316-
/>
317-
</SecretCreationStep>
318-
319-
<SecretCreationStep
320-
step={3}
321-
title="Name the secret"
322-
description="Give the secret a unique name so that it can be referenced in a connection config."
323-
>
324-
<FormField
325-
control={form.control}
326-
name="key"
327-
render={({ field }) => (
328-
<FormItem>
329-
<FormLabel>Key</FormLabel>
330-
<FormControl>
331-
<Input
332-
placeholder="my-github-token"
333-
{...field}
334-
/>
335-
</FormControl>
336-
<FormDescription>
337-
A unique name to identify this secret.
338-
</FormDescription>
339-
<FormMessage />
340-
</FormItem>
341-
)}
342-
/>
343-
</SecretCreationStep>
344-
345-
<div className="flex justify-end w-full">
346-
<Button
347-
type="submit"
348-
disabled={isSubmitting}
349-
>
350-
{isSubmitting && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
351-
Import Secret
352-
</Button>
353-
</div>
354-
</form>
355-
</Form>
356-
</DialogContent>
357-
</Dialog>
358-
)
359-
}
360-
361-
const GitHubPATCreationStep = ({ step }: { step: number }) => {
362-
return (
363-
<SecretCreationStep
364-
step={step}
365-
title="Create a Personal Access Token"
366-
description=<span>Navigate to <Link href="https://github.com/settings/tokens/new" target="_blank" className="underline">here on github.com</Link> (or your enterprise instance) and create a new personal access token. Sourcebot needs the <strong>repo</strong> scope in order to access private repositories:</span>
367-
>
368-
<Image
369-
className="mx-auto"
370-
src={githubPatCreation}
371-
alt="Create a personal access token"
372-
width={500}
373-
height={500}
374-
/>
375-
</SecretCreationStep>
376-
)
377-
}
378-
379-
const GitLabPATCreationStep = ({ step }: { step: number }) => {
380-
return (
381-
<SecretCreationStep
382-
step={step}
383-
title="Create a Personal Access Token"
384-
description="todo"
385-
>
386-
<p>todo</p>
387-
</SecretCreationStep>
388-
)
389-
}
390-
391-
const GiteaPATCreationStep = ({ step }: { step: number }) => {
392-
return (
393-
<SecretCreationStep
394-
step={step}
395-
title="Create a Personal Access Token"
396-
description="todo"
397-
>
398-
<p>todo</p>
399-
</SecretCreationStep>
400-
)
401-
}
402-
403-
interface SecretCreationStepProps {
404-
step: number;
405-
title: string;
406-
description: string | React.ReactNode;
407-
children: React.ReactNode;
408-
}
409-
410-
const SecretCreationStep = ({ step, title, description, children }: SecretCreationStepProps) => {
411-
return (
412-
<div className="relative flex flex-col gap-2">
413-
<div className="absolute -left-10 flex flex-col items-center gap-2 h-full">
414-
<span className="text-md font-semibold border rounded-full px-2">{step}</span>
415-
<Separator className="h-5/6" orientation="vertical" />
416-
</div>
417-
<h3 className="text-md font-semibold">
418-
{title}
419-
</h3>
420-
<p className="text-sm text-muted-foreground">
421-
{description}
422-
</p>
423-
{children}
424-
</div>
425-
)
426-
}

0 commit comments

Comments
 (0)