Skip to content

Commit 60b0ef6

Browse files
committed
implements credentials page
1 parent fa5fd4a commit 60b0ef6

15 files changed

Lines changed: 798 additions & 4 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- CreateEnum
2+
CREATE TYPE "CredentialType" AS ENUM ('GEMINI');
3+
4+
-- AlterTable
5+
ALTER TABLE "Node" ADD COLUMN "credentialId" TEXT;
6+
7+
-- CreateTable
8+
CREATE TABLE "Credential" (
9+
"id" TEXT NOT NULL,
10+
"name" TEXT NOT NULL,
11+
"value" TEXT NOT NULL,
12+
"type" "CredentialType" NOT NULL,
13+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14+
"updatedAt" TIMESTAMP(3) NOT NULL,
15+
"userId" TEXT NOT NULL,
16+
17+
CONSTRAINT "Credential_pkey" PRIMARY KEY ("id")
18+
);
19+
20+
-- AddForeignKey
21+
ALTER TABLE "Node" ADD CONSTRAINT "Node_credentialId_fkey" FOREIGN KEY ("credentialId") REFERENCES "Credential"("id") ON DELETE SET NULL ON UPDATE CASCADE;
22+
23+
-- AddForeignKey
24+
ALTER TABLE "Credential" ADD CONSTRAINT "Credential_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ model User {
1919
accounts Account[]
2020
Workflow Workflow[]
2121
22+
credentials Credential[]
23+
2224
@@map("user")
2325
}
2426

@@ -103,6 +105,9 @@ model Node {
103105
104106
outputConnections Connection[] @relation("FromNode")
105107
inputConnections Connection[] @relation("ToNode")
108+
109+
credentialId String?
110+
credentials Credential? @relation(fields: [credentialId], references: [id])
106111
}
107112

108113
model Connection {
@@ -123,3 +128,21 @@ model Connection {
123128
124129
@@unique([fromNodeId, toNodeId, fromOutput, toInput])
125130
}
131+
132+
model Credential {
133+
id String @id @default(cuid())
134+
name String
135+
value String
136+
type CredentialType
137+
138+
createdAt DateTime @default(now())
139+
updatedAt DateTime @updatedAt
140+
141+
userId String
142+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
143+
nodes Node[]
144+
}
145+
146+
enum CredentialType {
147+
GEMINI
148+
}
Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
import {
2+
CredentialError,
3+
CredentialLoading,
4+
CredentialView,
5+
} from "@/features/credentials/components/Credentials";
6+
import { prefetchCredential } from "@/features/credentials/server/prefetch";
17
import { requireAuth } from "@/lib/auth-utils";
8+
import { HydrateClient } from "@/trpc/server";
9+
import { Suspense } from "react";
10+
import { ErrorBoundary } from "react-error-boundary";
211

312
type Props = {
413
params: Promise<{ credentialId: string }>;
@@ -7,5 +16,20 @@ type Props = {
716
export default async function Page({ params }: Props) {
817
await requireAuth();
918
const { credentialId } = await params;
10-
return <p>Executions: {credentialId}</p>;
19+
20+
prefetchCredential(credentialId);
21+
22+
return (
23+
<div className="p-4 md:px-10 md:py-6 h-full">
24+
<div className="mx-auto max-w-md w-full flex flex-col gap-y-8 h-full">
25+
<HydrateClient>
26+
<ErrorBoundary fallback={<CredentialError />}>
27+
<Suspense fallback={<CredentialLoading />}>
28+
<CredentialView credentialId={credentialId} />
29+
</Suspense>
30+
</ErrorBoundary>
31+
</HydrateClient>
32+
</div>
33+
</div>
34+
);
1135
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import CredentialForm from "@/features/credentials/components/CredentialForm";
2+
import { requireAuth } from "@/lib/auth-utils";
3+
4+
export default async function Page() {
5+
await requireAuth();
6+
return (
7+
<div className="p-4 md:px-10 md:py-6 h-full">
8+
<div className="mx-auto max-w-md w-full flex flex-col gap-y-8 h-full">
9+
<CredentialForm />
10+
</div>
11+
</div>
12+
);
13+
}
Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,38 @@
1+
import {
2+
CredentialContainer,
3+
CredentialError,
4+
CredentialLoading,
5+
CredentialsList,
6+
} from "@/features/credentials/components/Credentials";
7+
import { params } from "@/features/credentials/server/params";
8+
import { prefetchCredentials } from "@/features/credentials/server/prefetch";
19
import { requireAuth } from "@/lib/auth-utils";
10+
import { HydrateClient } from "@/trpc/server";
11+
import { createLoader, type SearchParams } from "nuqs/server";
12+
import { Suspense } from "react";
13+
import { ErrorBoundary } from "react-error-boundary";
214

3-
export default async function Page() {
15+
const loader = createLoader(params);
16+
17+
type Props = {
18+
searchParams: Promise<SearchParams>;
19+
};
20+
21+
export default async function Page({ searchParams }: Props) {
422
await requireAuth();
5-
return <p>Credentials</p>;
23+
const params = await loader(searchParams);
24+
25+
prefetchCredentials(params);
26+
27+
return (
28+
<CredentialContainer>
29+
<HydrateClient>
30+
<ErrorBoundary fallback={<CredentialError />}>
31+
<Suspense fallback={<CredentialLoading />}>
32+
<CredentialsList />
33+
</Suspense>
34+
</ErrorBoundary>
35+
</HydrateClient>
36+
</CredentialContainer>
37+
);
638
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
"use client";
2+
3+
import { CredentialType } from "@prisma/client";
4+
import { useRouter } from "next/navigation";
5+
import {
6+
useCreateOneCredential,
7+
useUpdateCredential,
8+
} from "../hooks/useCredentials";
9+
import { useUpgradeModal } from "@/hooks/useUpgradeModal";
10+
import { FormProvider, useForm } from "react-hook-form";
11+
import { zodResolver } from "@hookform/resolvers/zod";
12+
import z from "zod";
13+
import {
14+
Card,
15+
CardContent,
16+
CardDescription,
17+
CardHeader,
18+
CardTitle,
19+
} from "@/components/ui/card";
20+
import {
21+
FormControl,
22+
FormField,
23+
FormItem,
24+
FormLabel,
25+
FormMessage,
26+
} from "@/components/ui/form";
27+
import { Input } from "@/components/ui/input";
28+
import {
29+
Select,
30+
SelectContent,
31+
SelectItem,
32+
SelectTrigger,
33+
SelectValue,
34+
} from "@/components/ui/select";
35+
import Image from "next/image";
36+
import { Button } from "@/components/ui/button";
37+
import Link from "next/link";
38+
39+
type Props = {
40+
initialData?: {
41+
id?: string;
42+
name: string;
43+
type: CredentialType;
44+
value: string;
45+
};
46+
};
47+
48+
const formSchema = z.object({
49+
name: z.string().min(1, "Name is required"),
50+
type: z.enum(CredentialType),
51+
value: z.string().min(1, "API key is required"),
52+
});
53+
54+
type FormValues = z.infer<typeof formSchema>;
55+
56+
const options = [
57+
{
58+
value: CredentialType.GEMINI,
59+
label: "Gemini",
60+
logo: "/logos/gemini.svg",
61+
},
62+
];
63+
64+
const CredentialForm = ({ initialData }: Props) => {
65+
const router = useRouter();
66+
const createOne = useCreateOneCredential();
67+
const update = useUpdateCredential();
68+
const { modal, handleError } = useUpgradeModal();
69+
70+
const isEdit = !!initialData?.id;
71+
72+
const form = useForm<FormValues>({
73+
resolver: zodResolver(formSchema),
74+
defaultValues: initialData || {
75+
name: "",
76+
type: CredentialType.GEMINI,
77+
value: "",
78+
},
79+
});
80+
81+
const onSubmit = async (values: FormValues) => {
82+
if (isEdit && initialData?.id) {
83+
await update.mutateAsync(
84+
{ id: initialData.id, ...values },
85+
{
86+
onSuccess: () => router.push(`/credentials`),
87+
},
88+
);
89+
} else {
90+
await createOne.mutateAsync(values, {
91+
onError: (error) => handleError(error),
92+
onSuccess: (data) => router.push(`/credentials/${data.id}`),
93+
});
94+
}
95+
};
96+
return (
97+
<>
98+
{modal}
99+
<Card>
100+
<CardHeader>
101+
<CardTitle>
102+
{isEdit ? "Edit Credentials" : "Create Credentials"}
103+
</CardTitle>
104+
<CardDescription>
105+
{isEdit
106+
? "Update your API key or credentials details"
107+
: "Add a new API key or credential to your account"}
108+
</CardDescription>
109+
</CardHeader>
110+
<CardContent>
111+
<FormProvider {...form}>
112+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
113+
<FormField
114+
control={form.control}
115+
name="name"
116+
render={({ field }) => (
117+
<FormItem>
118+
<FormLabel>Name</FormLabel>
119+
<FormControl>
120+
<Input placeholder="My API key" {...field} />
121+
</FormControl>
122+
<FormMessage />
123+
</FormItem>
124+
)}
125+
/>
126+
<FormField
127+
control={form.control}
128+
name="type"
129+
render={({ field }) => (
130+
<FormItem>
131+
<FormLabel>Type</FormLabel>
132+
<Select
133+
onValueChange={field.onChange}
134+
defaultValue={field.value}
135+
>
136+
<FormControl>
137+
<SelectTrigger className="w-full">
138+
<SelectValue />
139+
</SelectTrigger>
140+
</FormControl>
141+
<SelectContent>
142+
{options.map((o) => (
143+
<SelectItem key={o.value} value={o.value}>
144+
<div className="flex items-center gap-2">
145+
<Image
146+
src={o.logo}
147+
alt={o.label}
148+
width={16}
149+
height={16}
150+
/>
151+
{o.label}
152+
</div>
153+
</SelectItem>
154+
))}
155+
</SelectContent>
156+
</Select>
157+
<FormMessage />
158+
</FormItem>
159+
)}
160+
/>
161+
<FormField
162+
control={form.control}
163+
name="value"
164+
render={({ field }) => (
165+
<FormItem>
166+
<FormLabel>API Ley</FormLabel>
167+
<FormControl>
168+
<Input type="password" placeholder="sk-..." {...field} />
169+
</FormControl>
170+
<FormMessage />
171+
</FormItem>
172+
)}
173+
/>
174+
<div className="flex gap-4">
175+
<Button
176+
type="submit"
177+
disabled={createOne.isPending || update.isPending}
178+
>
179+
{isEdit ? "Update" : "Create"}
180+
</Button>
181+
<Button
182+
type="button"
183+
variant={"outline"}
184+
onClick={() => router.push("/credentials")}
185+
asChild
186+
>
187+
<Link href={"/credentials"} prefetch>
188+
Cancel
189+
</Link>
190+
</Button>
191+
</div>
192+
</form>
193+
</FormProvider>
194+
</CardContent>
195+
</Card>
196+
</>
197+
);
198+
};
199+
200+
export default CredentialForm;

0 commit comments

Comments
 (0)