Skip to content

Commit 224e405

Browse files
authored
feat: add secrets (#640)
1 parent f612772 commit 224e405

29 files changed

+1486
-17
lines changed

src/app/settings/LayoutSidebar.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { Avatar, AvatarFallback, AvatarImage, Skeleton } from "@zenml-io/react-c
55
import { SettingsMenu } from "./Menu";
66

77
export function DisplayServer() {
8-
const { data, isError, isPending } = useServerSettings({ throwOnError: true });
8+
const { data, isError, isPending } = useServerSettings({
9+
throwOnError: true
10+
});
911

1012
if (isPending) return <Skeleton className="h-9 w-full" />;
1113
if (isError) return null;
@@ -38,7 +40,9 @@ export function ServerSettingsMenu() {
3840
},
3941
{
4042
name: "Secrets",
41-
href: routes.settings.secrets.overview
43+
href: routes.settings.secrets.overview,
44+
isActiveOverride: (pathname: string) =>
45+
pathname.startsWith(routes.settings.secrets.overview)
4246
},
4347
{
4448
name: "Connectors",

src/app/settings/Menu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import NavLink from "@/components/NavLink";
33
type MenuItem = {
44
name: string;
55
href: string;
6+
isActiveOverride?: (pathname: string) => boolean;
67
};
78

89
type MenuProps = {
@@ -15,7 +16,7 @@ export function SettingsMenu({ items }: MenuProps) {
1516
<ul className="flex w-full flex-row flex-wrap items-center gap-1 lg:flex-col lg:items-start">
1617
{items.map((item) => (
1718
<li key={item.name} className="lg:w-full">
18-
<NavLink end to={item.href}>
19+
<NavLink end to={item.href} isActiveOverride={item.isActiveOverride}>
1920
{item.name}
2021
</NavLink>
2122
</li>
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import EyeIcon from "@/assets/icons/eye.svg?react";
2+
import Plus from "@/assets/icons/plus.svg?react";
3+
import Trash from "@/assets/icons/trash.svg?react";
4+
import { useCreateSecretMutation } from "@/data/secrets/create-secret-query";
5+
import { isFetchError } from "@/lib/fetch-error";
6+
import { zodResolver } from "@hookform/resolvers/zod";
7+
import { useQueryClient } from "@tanstack/react-query";
8+
import {
9+
Button,
10+
Dialog,
11+
DialogClose,
12+
DialogContent,
13+
DialogFooter,
14+
DialogHeader,
15+
DialogTitle,
16+
DialogTrigger,
17+
Input,
18+
useToast
19+
} from "@zenml-io/react-component-library";
20+
import { useState } from "react";
21+
import { Controller, useFieldArray, useForm } from "react-hook-form";
22+
import { secretFormSchema, SecretFormType } from "./form-schema";
23+
24+
interface Workspace {
25+
id: string;
26+
}
27+
28+
export function AddSecretDialog({ id, workspace }: { id: string; workspace: Workspace }) {
29+
const [open, setOpen] = useState(false);
30+
31+
return (
32+
<Dialog
33+
open={open}
34+
onOpenChange={(val) => {
35+
setOpen(val);
36+
}}
37+
>
38+
<DialogTrigger asChild>
39+
<Button className="shrink-0" intent="primary">
40+
Add secret
41+
</Button>
42+
</DialogTrigger>
43+
<DialogContent className="mx-auto w-[90vw] max-w-[744px]">
44+
<DialogHeader>
45+
<DialogTitle>Register New Secret</DialogTitle>
46+
</DialogHeader>
47+
<AddSecret userId={id} setOpen={setOpen} workspaceId={workspace.id} />
48+
</DialogContent>
49+
</Dialog>
50+
);
51+
}
52+
53+
export function AddSecret({
54+
userId,
55+
setOpen,
56+
workspaceId
57+
}: {
58+
userId: string;
59+
setOpen: (open: boolean) => void;
60+
workspaceId: string;
61+
}) {
62+
const {
63+
handleSubmit,
64+
control,
65+
watch,
66+
setValue,
67+
formState: { isValid },
68+
reset
69+
} = useForm<SecretFormType>({
70+
resolver: zodResolver(secretFormSchema),
71+
defaultValues: {
72+
secretName: "",
73+
keysValues: [{ key: "", value: "" }]
74+
}
75+
});
76+
77+
const { fields, append, remove } = useFieldArray({
78+
control,
79+
name: "keysValues"
80+
});
81+
82+
const { toast } = useToast();
83+
const queryClient = useQueryClient();
84+
const { mutate } = useCreateSecretMutation({
85+
onError(error) {
86+
if (isFetchError(error)) {
87+
toast({
88+
status: "error",
89+
emphasis: "subtle",
90+
description: error.message,
91+
rounded: true
92+
});
93+
}
94+
},
95+
onSuccess() {
96+
queryClient.invalidateQueries({ queryKey: ["secrets"] });
97+
setOpen(false);
98+
reset();
99+
}
100+
});
101+
102+
const postSecret = (data: SecretFormType) => {
103+
mutate({
104+
user: userId,
105+
workspace: workspaceId,
106+
name: data.secretName,
107+
scope: "workspace",
108+
values: data.keysValues.reduce(
109+
(acc, pair) => {
110+
if (pair.key && pair.value) acc[pair.key] = pair.value;
111+
return acc;
112+
},
113+
{} as Record<string, string>
114+
)
115+
});
116+
};
117+
118+
const onSubmit = (data: SecretFormType) => {
119+
postSecret(data);
120+
};
121+
122+
return (
123+
<>
124+
<form id="create-secret-form" className="gap-5 p-5" onSubmit={handleSubmit(onSubmit)}>
125+
<div className="space-y-1">
126+
<div className="space-y-0.5">
127+
<label className="font-inter text-sm text-left font-medium leading-5">
128+
Secret Name
129+
<span className="ml-1 text-theme-text-error">*</span>
130+
</label>
131+
<Controller
132+
name="secretName"
133+
control={control}
134+
render={({ field }) => <Input {...field} className="mb-3 w-full" required />}
135+
/>
136+
</div>
137+
<div className="mt-10">
138+
<div>
139+
<h1 className="font-inter text-lg text-left font-semibold ">Keys</h1>
140+
</div>
141+
<div className="mt-5 flex flex-row ">
142+
<div className="flex-grow">
143+
<label className="font-inter text-sm text-left font-medium">Key</label>
144+
</div>
145+
<div className="flex-grow pr-12">
146+
<label className="font-inter text-sm text-left font-medium">Value</label>
147+
</div>
148+
</div>
149+
</div>
150+
151+
{fields.map((field, index) => (
152+
<div key={field.id} className="flex flex-row items-center space-x-1 ">
153+
<div className="relative flex-grow ">
154+
<Controller
155+
name={`keysValues.${index}.key`}
156+
control={control}
157+
render={({ field }) => (
158+
<Input {...field} className="mb-2 w-full" required placeholder="key" />
159+
)}
160+
/>
161+
</div>
162+
<div className="relative flex-grow">
163+
<div className="relative">
164+
<Controller
165+
name={`keysValues.${index}.value`}
166+
control={control}
167+
render={({ field }) => (
168+
<Input
169+
{...field}
170+
className="mb-2 w-full pr-10"
171+
required
172+
placeholder="•••••••••"
173+
type={watch(`keysValues.${index}.showPassword`) ? "text" : "password"}
174+
/>
175+
)}
176+
/>
177+
<div
178+
onClick={() => {
179+
const showPassword = watch(`keysValues.${index}.showPassword`);
180+
setValue(`keysValues.${index}.showPassword`, !showPassword);
181+
}}
182+
className="absolute inset-y-1 right-0 flex cursor-pointer items-center pb-1 pr-3"
183+
>
184+
<EyeIcon className="h-4 w-4 flex-shrink-0 cursor-pointer" />
185+
</div>
186+
</div>
187+
</div>
188+
<div className="flex items-center">
189+
{index === fields.length - 1 && (
190+
<Button
191+
intent="primary"
192+
emphasis="subtle"
193+
onClick={() => append({ key: "", value: "" })}
194+
className="mb-2 flex h-7 w-7 items-center justify-center"
195+
>
196+
<Plus className="flex-shrink-0 fill-primary-600" />
197+
</Button>
198+
)}
199+
{index !== fields.length - 1 && (
200+
<Button
201+
intent="secondary"
202+
emphasis="minimal"
203+
onClick={() => remove(index)}
204+
className="mb-2 h-7 w-7 items-center justify-center"
205+
>
206+
<Trash className="flex-shrink-0 fill-theme-text-secondary" />
207+
</Button>
208+
)}
209+
</div>
210+
</div>
211+
))}
212+
</div>
213+
</form>
214+
<DialogFooter className="gap-[10px]">
215+
<DialogClose asChild>
216+
<Button size="sm" intent="secondary">
217+
Cancel
218+
</Button>
219+
</DialogClose>
220+
<Button intent="primary" disabled={!isValid} type="submit" form="create-secret-form">
221+
Register Secret
222+
</Button>
223+
</DialogFooter>
224+
</>
225+
);
226+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useState } from "react";
2+
import { useQueryClient } from "@tanstack/react-query";
3+
import {
4+
Button,
5+
Input,
6+
Dialog,
7+
DialogTrigger,
8+
DialogContent,
9+
DialogHeader,
10+
DialogTitle,
11+
DialogFooter,
12+
DialogClose
13+
} from "@zenml-io/react-component-library";
14+
import { useDeleteSecret } from "@/data/secrets/delete-secret-query";
15+
16+
export function DeleteSecretAlert({
17+
secretId,
18+
isOpen,
19+
onClose
20+
}: {
21+
secretId: string;
22+
isOpen: boolean;
23+
onClose: () => void;
24+
}) {
25+
const queryClient = useQueryClient();
26+
const { mutate } = useDeleteSecret({
27+
onSuccess() {
28+
queryClient.invalidateQueries({ queryKey: ["secrets"] });
29+
}
30+
});
31+
32+
const [inputValue, setInputValue] = useState("");
33+
34+
function deleteSecret() {
35+
mutate(secretId);
36+
}
37+
38+
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
39+
setInputValue(event.target.value);
40+
}
41+
42+
return (
43+
<Dialog open={isOpen} onOpenChange={onClose}>
44+
<DialogTrigger asChild>
45+
<Button className="shrink-0" intent="primary">
46+
Delete Secret
47+
</Button>
48+
</DialogTrigger>
49+
<DialogContent>
50+
<DialogHeader>
51+
<DialogTitle>Delete secret</DialogTitle>
52+
</DialogHeader>
53+
<div className="gap-5 p-5">
54+
<p className="text-text-md text-theme-text-secondary">
55+
Are you sure you want to delete this secret?
56+
</p>
57+
<p className="text-text-md text-theme-text-secondary">This action cannot be undone.</p>
58+
<h3 className="font-inter text-sm mb-1 mt-4 text-left font-medium leading-5">
59+
Please type DELETE to confirm
60+
</h3>
61+
<Input
62+
name="key"
63+
onChange={handleInputChange}
64+
className="w-full"
65+
required
66+
value={inputValue}
67+
/>
68+
</div>
69+
<DialogFooter className="gap-[10px]">
70+
<DialogClose asChild>
71+
<Button size="sm" intent="secondary">
72+
Cancel
73+
</Button>
74+
</DialogClose>
75+
<Button
76+
intent="danger"
77+
type="submit"
78+
form="edit-secret-form"
79+
onClick={deleteSecret}
80+
disabled={inputValue !== "DELETE"}
81+
>
82+
Delete
83+
</Button>
84+
</DialogFooter>
85+
</DialogContent>
86+
</Dialog>
87+
);
88+
}

0 commit comments

Comments
 (0)