feat: link field with application

This commit is contained in:
Lorenzo Migliorero
2024-07-25 19:32:02 +02:00
parent f866250c25
commit d243470029
10 changed files with 3218 additions and 225 deletions

View File

@@ -1,13 +1,5 @@
import { AddSSHKey } from "@/components/dashboard/settings/ssh-keys/add-ssh-key";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { import {
Form, Form,
FormControl, FormControl,
@@ -17,12 +9,24 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard"; import { SelectSeparator } from "@radix-ui/react-select";
import { CopyIcon, LockIcon } from "lucide-react"; import { KeyRoundIcon, LockIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
import { flushSync } from "react-dom";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -33,6 +37,7 @@ const GitProviderSchema = z.object({
}), }),
branch: z.string().min(1, "Branch required"), branch: z.string().min(1, "Branch required"),
buildPath: z.string().min(1, "Build Path required"), buildPath: z.string().min(1, "Build Path required"),
sshKey: z.string(),
}); });
type GitProvider = z.infer<typeof GitProviderSchema>; type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -43,19 +48,22 @@ interface Props {
export const SaveGitProvider = ({ applicationId }: Props) => { export const SaveGitProvider = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId }); const { data, refetch } = api.application.one.useQuery({ applicationId });
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.application.saveGitProdiver.useMutation(); api.application.saveGitProdiver.useMutation();
const { mutateAsync: generateSSHKey, isLoading: isGeneratingSSHKey } = // const { mutateAsync: generateSSHKey, isLoading: isGeneratingSSHKey } =
api.application.generateSSHKey.useMutation(); // api.application.generateSSHKey.useMutation();
const { mutateAsync: removeSSHKey, isLoading: isRemovingSSHKey } = // const { mutateAsync: removeSSHKey, isLoading: isRemovingSSHKey } =
api.application.removeSSHKey.useMutation(); // api.application.removeSSHKey.useMutation();
const form = useForm<GitProvider>({ const form = useForm<GitProvider>({
defaultValues: { defaultValues: {
branch: "", branch: "",
buildPath: "/", buildPath: "/",
repositoryURL: "", repositoryURL: "",
sshKey: "",
}, },
resolver: zodResolver(GitProviderSchema), resolver: zodResolver(GitProviderSchema),
}); });
@@ -63,6 +71,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
sshKey: data.customGitSSHKeyId || "",
branch: data.customGitBranch || "", branch: data.customGitBranch || "",
buildPath: data.customGitBuildPath || "/", buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "", repositoryURL: data.customGitUrl || "",
@@ -75,6 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitBranch: values.branch, customGitBranch: values.branch,
customGitBuildPath: values.buildPath, customGitBuildPath: values.buildPath,
customGitUrl: values.repositoryURL, customGitUrl: values.repositoryURL,
customGitSSHKeyId: values.sshKey,
applicationId, applicationId,
}) })
.then(async () => { .then(async () => {
@@ -93,116 +103,14 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
className="flex flex-col gap-4" className="flex flex-col gap-4"
> >
<div className="grid md:grid-cols-2 gap-4"> <div className="grid md:grid-cols-2 gap-4">
<div className="md:col-span-2 space-y-4"> <div className="flex items-end col-span-2 gap-4">
<div className="grow">
<FormField <FormField
control={form.control} control={form.control}
name="repositoryURL" name="repositoryURL"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="flex flex-row justify-between"> <FormLabel>Repository URL</FormLabel>
Repository URL
<div className="flex gap-2">
<Dialog>
<DialogTrigger className="flex flex-row gap-2">
<LockIcon className="size-4 text-muted-foreground" />?
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Private Repository</DialogTitle>
<DialogDescription>
If your repository is private is necessary to
generate SSH Keys to add to your git provider.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="relative">
<Textarea
placeholder="Please click on Generate SSH Key"
className="no-scrollbar h-64 text-muted-foreground"
disabled={!data?.customGitSSHKey}
contentEditable={false}
value={
data?.customGitSSHKey ||
"Please click on Generate SSH Key"
}
/>
<button
type="button"
className="absolute right-2 top-2"
onClick={() => {
copy(
data?.customGitSSHKey ||
"Generate a SSH Key",
);
toast.success("SSH Copied to clipboard");
}}
>
<CopyIcon className="size-4" />
</button>
</div>
</div>
<DialogFooter className="flex sm:justify-between gap-3.5 flex-col sm:flex-col w-full">
<div className="flex flex-row gap-2 w-full justify-between flex-wrap">
{data?.customGitSSHKey && (
<Button
variant="destructive"
isLoading={
isGeneratingSSHKey || isRemovingSSHKey
}
className="max-sm:w-full"
onClick={async () => {
await removeSSHKey({
applicationId,
})
.then(async () => {
toast.success("SSH Key Removed");
await refetch();
})
.catch(() => {
toast.error(
"Error to remove the SSH Key",
);
});
}}
type="button"
>
Remove SSH Key
</Button>
)}
<Button
isLoading={
isGeneratingSSHKey || isRemovingSSHKey
}
className="max-sm:w-full"
onClick={async () => {
await generateSSHKey({
applicationId,
})
.then(async () => {
toast.success("SSH Key Generated");
await refetch();
})
.catch(() => {
toast.error(
"Error to generate the SSH Key",
);
});
}}
type="button"
>
Generate SSH Key
</Button>
</div>
<span className="text-sm text-muted-foreground">
Is recommended to remove the SSH Key if you want
to deploy a public repository.
</span>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="git@bitbucket.org" {...field} /> <Input placeholder="git@bitbucket.org" {...field} />
</FormControl> </FormControl>
@@ -211,7 +119,55 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
)} )}
/> />
</div> </div>
<div className="space-y-4"> {sshKeys && sshKeys.length > 0 ? (
<FormField
control={form.control}
name="sshKey"
render={({ field }) => (
<FormItem className="basis-40">
<FormLabel className="w-full inline-flex justify-between">
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl>
<Select
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
<SelectSeparator />
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
) : (
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
)}
</div>
<FormField <FormField
control={form.control} control={form.control}
name="branch" name="branch"
@@ -225,8 +181,6 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
</div>
<div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="buildPath" name="buildPath"
@@ -241,11 +195,10 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
)} )}
/> />
</div> </div>
</div>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<Button type="submit" className="w-fit" isLoading={isLoading}> <Button type="submit" className="w-fit" isLoading={isLoading}>
Save{" "} Save
</Button> </Button>
</div> </div>
</form> </form>

View File

@@ -127,6 +127,7 @@ export const AddSSHKey = ({ children }: Props) => {
<FormControl> <FormControl>
<Textarea <Textarea
placeholder={"-----BEGIN RSA PRIVATE KEY-----"} placeholder={"-----BEGIN RSA PRIVATE KEY-----"}
rows={5}
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -143,16 +144,13 @@ export const AddSSHKey = ({ children }: Props) => {
<FormLabel>Public Key</FormLabel> <FormLabel>Public Key</FormLabel>
</div> </div>
<FormControl> <FormControl>
<Textarea <Input placeholder={"ssh-rsa AAAAB3NzaC1yc2E"} {...field} />
placeholder={"ssh-rsa AAAAB3NzaC1yc2E"}
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<DialogFooter className="flex w-full flex-row !justify-between pt-3"> <DialogFooter>
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">
Create Create
</Button> </Button>

View File

@@ -39,6 +39,7 @@ export const ShowDestinations = () => {
</AddSSHKey> </AddSSHKey>
</div> </div>
) : ( ) : (
<div className="space-y-8">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex gap-4 text-xs px-3.5"> <div className="flex gap-4 text-xs px-3.5">
<div className="col-span-2 basis-4/12">Key</div> <div className="col-span-2 basis-4/12">Key</div>
@@ -80,14 +81,13 @@ export const ShowDestinations = () => {
</div> </div>
</div> </div>
))} ))}
<div> </div>
<AddSSHKey> <AddSSHKey>
<Button> <Button>
<KeyRoundIcon className="size-4" /> Add SSH Key <KeyRoundIcon className="size-4" /> Add SSH Key
</Button> </Button>
</AddSSHKey> </AddSSHKey>
</div> </div>
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,3 +1,4 @@
import { DeleteSSHKey } from "@/components/dashboard/settings/ssh-keys/delete-ssh-key";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -135,7 +136,7 @@ export const UpdateSSHKey = ({ children, sshKeyId = "" }: Props) => {
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
<DialogFooter className="flex w-full flex-row !justify-between pt-3"> <DialogFooter>
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">
Update Update
</Button> </Button>

View File

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS "ssh-key" (
"sshKeyId" text PRIMARY KEY NOT NULL,
"publicKey" text NOT NULL,
"name" text NOT NULL,
"description" text,
"createdAt" text NOT NULL,
"lastUsedAt" text
);
--> statement-breakpoint
ALTER TABLE "application" RENAME COLUMN "customGitSSHKey" TO "customGitSSHKeyId";--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "application" ADD CONSTRAINT "application_customGitSSHKeyId_ssh-key_sshKeyId_fk" FOREIGN KEY ("customGitSSHKeyId") REFERENCES "public"."ssh-key"("sshKeyId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -183,6 +183,13 @@
"when": 1721633853118, "when": 1721633853118,
"tag": "0025_lying_mephisto", "tag": "0025_lying_mephisto",
"breakpoints": true "breakpoints": true
},
{
"idx": 26,
"version": "6",
"when": 1721928706816,
"tag": "0026_yielding_king_cobra",
"breakpoints": true
} }
] ]
} }

View File

@@ -235,6 +235,7 @@ export const applicationRouter = createTRPCRouter({
customGitBranch: input.customGitBranch, customGitBranch: input.customGitBranch,
customGitBuildPath: input.customGitBuildPath, customGitBuildPath: input.customGitBuildPath,
customGitUrl: input.customGitUrl, customGitUrl: input.customGitUrl,
customGitSSHKeyId: input.customGitSSHKeyId,
sourceType: "git", sourceType: "git",
applicationStatus: "idle", applicationStatus: "idle",
}); });
@@ -249,9 +250,9 @@ export const applicationRouter = createTRPCRouter({
await generateSSHKey(application.appName); await generateSSHKey(application.appName);
const file = await readRSAFile(application.appName); const file = await readRSAFile(application.appName);
await updateApplication(input.applicationId, { // await updateApplication(input.applicationId, {
customGitSSHKey: file, // customGitSSHKey: file,
}); // });
} catch (error) {} } catch (error) {}
return true; return true;
@@ -261,9 +262,9 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId); const application = await findApplicationById(input.applicationId);
await removeRSAFiles(application.appName); await removeRSAFiles(application.appName);
await updateApplication(input.applicationId, { // await updateApplication(input.applicationId, {
customGitSSHKey: null, // customGitSSHKey: null,
}); // });
return true; return true;
}), }),

View File

@@ -20,6 +20,7 @@ import { redirects } from "./redirects";
import { registry } from "./registry"; import { registry } from "./registry";
import { security } from "./security"; import { security } from "./security";
import { applicationStatus } from "./shared"; import { applicationStatus } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const sourceType = pgEnum("sourceType", [ export const sourceType = pgEnum("sourceType", [
@@ -132,7 +133,12 @@ export const applications = pgTable("application", {
customGitUrl: text("customGitUrl"), customGitUrl: text("customGitUrl"),
customGitBranch: text("customGitBranch"), customGitBranch: text("customGitBranch"),
customGitBuildPath: text("customGitBuildPath"), customGitBuildPath: text("customGitBuildPath"),
customGitSSHKey: text("customGitSSHKey"), customGitSSHKeyId: text("customGitSSHKeyId").references(
() => sshKeys.sshKeyId,
{
onDelete: "set null",
},
),
dockerfile: text("dockerfile"), dockerfile: text("dockerfile"),
// Drop // Drop
dropBuildPath: text("dropBuildPath"), dropBuildPath: text("dropBuildPath"),
@@ -170,6 +176,10 @@ export const applicationsRelations = relations(
references: [projects.projectId], references: [projects.projectId],
}), }),
deployments: many(deployments), deployments: many(deployments),
customGitSSHKey: one(sshKeys, {
fields: [applications.customGitSSHKeyId],
references: [sshKeys.sshKeyId],
}),
domains: many(domains), domains: many(domains),
mounts: many(mounts), mounts: many(mounts),
redirects: many(redirects), redirects: many(redirects),
@@ -289,7 +299,7 @@ const createSchema = createInsertSchema(applications, {
dockerImage: z.string().optional(), dockerImage: z.string().optional(),
username: z.string().optional(), username: z.string().optional(),
password: z.string().optional(), password: z.string().optional(),
customGitSSHKey: z.string().optional(), customGitSSHKeyId: z.string().optional(),
repository: z.string().optional(), repository: z.string().optional(),
dockerfile: z.string().optional(), dockerfile: z.string().optional(),
branch: z.string().optional(), branch: z.string().optional(),
@@ -371,7 +381,12 @@ export const apiSaveGitProvider = createSchema
customGitBuildPath: true, customGitBuildPath: true,
customGitUrl: true, customGitUrl: true,
}) })
.required(); .required()
.merge(
createSchema.pick({
customGitSSHKeyId: true,
}),
);
export const apiSaveEnvironmentVariables = createSchema export const apiSaveEnvironmentVariables = createSchema
.pick({ .pick({

View File

@@ -1,4 +1,6 @@
import { applications } from "@/server/db/schema/application";
import { sshKeyCreate } from "@/server/db/validations"; import { sshKeyCreate } from "@/server/db/validations";
import { relations } from "drizzle-orm";
import { pgTable, text, time } from "drizzle-orm/pg-core"; import { pgTable, text, time } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@@ -17,6 +19,10 @@ export const sshKeys = pgTable("ssh-key", {
lastUsedAt: text("lastUsedAt"), lastUsedAt: text("lastUsedAt"),
}); });
export const sshKeysRelations = relations(sshKeys, ({ many }) => ({
applications: many(applications),
}));
const createSchema = createInsertSchema( const createSchema = createInsertSchema(
sshKeys, sshKeys,
/* Private key is not stored in the DB */ /* Private key is not stored in the DB */