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 {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
@@ -17,12 +9,24 @@ import {
FormMessage,
} from "@/components/ui/form";
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 { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { CopyIcon, LockIcon } from "lucide-react";
import { SelectSeparator } from "@radix-ui/react-select";
import { KeyRoundIcon, LockIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { flushSync } from "react-dom";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
@@ -33,6 +37,7 @@ const GitProviderSchema = z.object({
}),
branch: z.string().min(1, "Branch required"),
buildPath: z.string().min(1, "Build Path required"),
sshKey: z.string(),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -43,19 +48,22 @@ interface Props {
export const SaveGitProvider = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { data: sshKeys } = api.sshKey.all.useQuery();
const router = useRouter();
const { mutateAsync, isLoading } =
api.application.saveGitProdiver.useMutation();
const { mutateAsync: generateSSHKey, isLoading: isGeneratingSSHKey } =
api.application.generateSSHKey.useMutation();
// const { mutateAsync: generateSSHKey, isLoading: isGeneratingSSHKey } =
// api.application.generateSSHKey.useMutation();
const { mutateAsync: removeSSHKey, isLoading: isRemovingSSHKey } =
api.application.removeSSHKey.useMutation();
// const { mutateAsync: removeSSHKey, isLoading: isRemovingSSHKey } =
// api.application.removeSSHKey.useMutation();
const form = useForm<GitProvider>({
defaultValues: {
branch: "",
buildPath: "/",
repositoryURL: "",
sshKey: "",
},
resolver: zodResolver(GitProviderSchema),
});
@@ -63,6 +71,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
useEffect(() => {
if (data) {
form.reset({
sshKey: data.customGitSSHKeyId || "",
branch: data.customGitBranch || "",
buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "",
@@ -75,6 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitBranch: values.branch,
customGitBuildPath: values.buildPath,
customGitUrl: values.repositoryURL,
customGitSSHKeyId: values.sshKey,
applicationId,
})
.then(async () => {
@@ -92,160 +102,103 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid md:grid-cols-2 gap-4 ">
<div className="md:col-span-2 space-y-4">
<FormField
control={form.control}
name="repositoryURL"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row justify-between">
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");
}}
<div className="grid md:grid-cols-2 gap-4">
<div className="flex items-end col-span-2 gap-4">
<div className="grow">
<FormField
control={form.control}
name="repositoryURL"
render={({ field }) => (
<FormItem>
<FormLabel>Repository URL</FormLabel>
<FormControl>
<Input placeholder="git@bitbucket.org" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{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}
>
<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>
<Input placeholder="git@bitbucket.org" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem>
<FormLabel>Branch</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{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
control={form.control}
name="branch"
render={({ field }) => (
<FormItem>
<FormLabel>Branch</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end">
<Button type="submit" className="w-fit" isLoading={isLoading}>
Save{" "}
Save
</Button>
</div>
</form>

View File

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

View File

@@ -39,54 +39,54 @@ export const ShowDestinations = () => {
</AddSSHKey>
</div>
) : (
<div className="flex flex-col gap-4">
<div className="flex gap-4 text-xs px-3.5">
<div className="col-span-2 basis-4/12">Key</div>
<div className="basis-3/12">Added</div>
<div>Last Used</div>
</div>
{data?.map((sshKey) => (
<div
key={sshKey.sshKeyId}
className="flex gap-4 items-center border p-3.5 rounded-lg text-sm"
>
<div className="flex flex-col basis-4/12">
<span>{sshKey.name}</span>
{sshKey.description && (
<span className="text-xs text-muted-foreground">
{sshKey.description}
</span>
)}
</div>
<div className="basis-3/12">
{formatDistanceToNow(new Date(sshKey.createdAt), {
addSuffix: true,
})}
</div>
<div className="grow">
{sshKey.lastUsedAt
? formatDistanceToNow(new Date(sshKey.lastUsedAt), {
addSuffix: true,
})
: "Never"}
</div>
<div className="flex flex-row gap-1">
<UpdateSSHKey sshKeyId={sshKey.sshKeyId}>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</UpdateSSHKey>
<DeleteSSHKey sshKeyId={sshKey.sshKeyId} />
</div>
<div className="space-y-8">
<div className="flex flex-col gap-4">
<div className="flex gap-4 text-xs px-3.5">
<div className="col-span-2 basis-4/12">Key</div>
<div className="basis-3/12">Added</div>
<div>Last Used</div>
</div>
))}
<div>
<AddSSHKey>
<Button>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
</AddSSHKey>
{data?.map((sshKey) => (
<div
key={sshKey.sshKeyId}
className="flex gap-4 items-center border p-3.5 rounded-lg text-sm"
>
<div className="flex flex-col basis-4/12">
<span>{sshKey.name}</span>
{sshKey.description && (
<span className="text-xs text-muted-foreground">
{sshKey.description}
</span>
)}
</div>
<div className="basis-3/12">
{formatDistanceToNow(new Date(sshKey.createdAt), {
addSuffix: true,
})}
</div>
<div className="grow">
{sshKey.lastUsedAt
? formatDistanceToNow(new Date(sshKey.lastUsedAt), {
addSuffix: true,
})
: "Never"}
</div>
<div className="flex flex-row gap-1">
<UpdateSSHKey sshKeyId={sshKey.sshKeyId}>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</UpdateSSHKey>
<DeleteSSHKey sshKeyId={sshKey.sshKeyId} />
</div>
</div>
))}
</div>
<AddSSHKey>
<Button>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
</AddSSHKey>
</div>
)}
</CardContent>

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 { Button } from "@/components/ui/button";
import {
@@ -135,7 +136,7 @@ export const UpdateSSHKey = ({ children, sshKeyId = "" }: Props) => {
<FormMessage />
</FormItem>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
<DialogFooter>
<Button isLoading={isLoading} type="submit">
Update
</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,
"tag": "0025_lying_mephisto",
"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,
customGitBuildPath: input.customGitBuildPath,
customGitUrl: input.customGitUrl,
customGitSSHKeyId: input.customGitSSHKeyId,
sourceType: "git",
applicationStatus: "idle",
});
@@ -249,9 +250,9 @@ export const applicationRouter = createTRPCRouter({
await generateSSHKey(application.appName);
const file = await readRSAFile(application.appName);
await updateApplication(input.applicationId, {
customGitSSHKey: file,
});
// await updateApplication(input.applicationId, {
// customGitSSHKey: file,
// });
} catch (error) {}
return true;
@@ -261,9 +262,9 @@ export const applicationRouter = createTRPCRouter({
.mutation(async ({ input }) => {
const application = await findApplicationById(input.applicationId);
await removeRSAFiles(application.appName);
await updateApplication(input.applicationId, {
customGitSSHKey: null,
});
// await updateApplication(input.applicationId, {
// customGitSSHKey: null,
// });
return true;
}),

View File

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

View File

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