Feat/monorepo (#292)

* feat(create-turbo): apply official-starter transform

* refactor: move folder

* wip: monorepo

* feat: add builf

* refactor: add pr

* update

* add .env

* refactor: update build

* refactor: update build docker

* refactor: add progress plain

* refactor: remove node pty

* refactor: remove

* remove

* refactor: update

* refacotr: uopdate

* refactor: add remix app

* add env

* refactor: add pnpm start

* refactor: remove

* refactor: remove folders

* refactor: remove .dockerfile

* chore: update biome

* test

* choe: add husky

* remove .docker folder

* feat: add docs website

* refactor: add husky

* chore(version): bump version

* refactor: add new changes

* refactor: update circle path

* refactor: update

* refactor: update

* refactor: update dockerfile

* refactor: update dockerfile

* refactor: update command

* refactor: update

* refactor: update dockerfile

* refactor: add tsx

* refactor: update dockerfile

* refactor: add deps

* refactor: up[date

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: yuodate

* refactor: remove

* refactor: uncomment

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: updare

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: imprt

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: remove

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: change path

* refactor: update

* refactor: update

* refactor: upoadte

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: add

* refactor: update

* refactor: update

* refactor: add

* refactor: update

* refactor: remove

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: removed

* refactor: update

* refactor: update

* refactor: update

* refactor: add config

* refactor: update

* refactor: add

* refactor: update

* refactor: update

* refactor: remove

* refactor: update

* refactor: remove

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: add docs

* refactor: update

* refactor: add website

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: add ignore builds

* refactor: update

* refactor: update

* refactor: add

* refactor: update

* refactor: update

* refactor: remove needs

* refactor: update

* refactor: update

* refactor: add config

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: remove

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: add

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: upodate

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update package json

* refactor: add biome

* refactor: add sponsors

* refactor: update

* refactor: update

* refactor: remove

* refactor: update

* refactor: update

* refactor: update

* refactor: update scripts

* refactor: remove

* refactor: update

* refactor: remove

---------

Co-authored-by: Turbobot <turbobot@vercel.com>
This commit is contained in:
Mauricio Siu
2024-07-29 23:08:23 -06:00
committed by GitHub
parent 9eba4f8b84
commit 5280c861e8
942 changed files with 87366 additions and 6709 deletions

View File

@@ -0,0 +1,128 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { ShowVolumes } from "../volumes/show-volumes";
import { ShowMysqlResources } from "./show-mysql-resources";
const addDockerImage = z.object({
dockerImage: z.string().min(1, "Docker image is required"),
command: z.string(),
});
interface Props {
mysqlId: string;
}
type AddDockerImage = z.infer<typeof addDockerImage>;
export const ShowAdvancedMysql = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery(
{
mysqlId,
},
{ enabled: !!mysqlId },
);
const { mutateAsync } = api.mysql.update.useMutation();
const form = useForm<AddDockerImage>({
defaultValues: {
dockerImage: "",
command: "",
},
resolver: zodResolver(addDockerImage),
});
useEffect(() => {
if (data) {
form.reset({
dockerImage: data.dockerImage,
command: data.command || "",
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: AddDockerImage) => {
await mutateAsync({
mysqlId,
dockerImage: formData?.dockerImage,
command: formData?.command,
})
.then(async () => {
toast.success("Resources Updated");
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
});
};
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Advanced Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 "
>
<div className="grid w-full gap-4">
<FormField
control={form.control}
name="dockerImage"
render={({ field }) => (
<FormItem>
<FormLabel>Docker Image</FormLabel>
<FormControl>
<Input placeholder="mysql:16" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>Command</FormLabel>
<FormControl>
<Input placeholder="Custom command" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button isLoading={form.formState.isSubmitting} type="submit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
<ShowVolumes mysqlId={mysqlId} />
<ShowMysqlResources mysqlId={mysqlId} />
</div>
</>
);
};

View File

@@ -0,0 +1,238 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addResourcesMysql = z.object({
memoryReservation: z.number().nullable().optional(),
cpuLimit: z.number().nullable().optional(),
memoryLimit: z.number().nullable().optional(),
cpuReservation: z.number().nullable().optional(),
});
interface Props {
mysqlId: string;
}
type AddResourcesMysql = z.infer<typeof addResourcesMysql>;
export const ShowMysqlResources = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery(
{
mysqlId,
},
{ enabled: !!mysqlId },
);
const { mutateAsync, isLoading } = api.mysql.update.useMutation();
const form = useForm<AddResourcesMysql>({
defaultValues: {},
resolver: zodResolver(addResourcesMysql),
});
useEffect(() => {
if (data) {
form.reset({
cpuLimit: data?.cpuLimit || undefined,
cpuReservation: data?.cpuReservation || undefined,
memoryLimit: data?.memoryLimit || undefined,
memoryReservation: data?.memoryReservation || undefined,
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: AddResourcesMysql) => {
await mutateAsync({
mysqlId,
cpuLimit: formData.cpuLimit || null,
cpuReservation: formData.cpuReservation || null,
memoryLimit: formData.memoryLimit || null,
memoryReservation: formData.memoryReservation || null,
})
.then(async () => {
toast.success("Resources Updated");
await refetch();
})
.catch(() => {
toast.error("Error to Update the resources");
});
};
return (
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Resources</CardTitle>
<CardDescription>
If you want to decrease or increase the resources to a specific
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<AlertBlock type="info">
Please remember to click Redeploy after modify the resources to apply
the changes.
</AlertBlock>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="grid w-full md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Reservation</FormLabel>
<FormControl>
<Input
placeholder="256 MB"
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryLimit"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Memory Limit</FormLabel>
<FormControl>
<Input
placeholder={"1024 MB"}
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="cpuLimit"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Limit</FormLabel>
<FormControl>
<Input
placeholder={"2"}
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="cpuReservation"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Cpu Reservation</FormLabel>
<FormControl>
<Input
placeholder={"1"}
{...field}
value={field.value?.toString() || ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
// Si el campo está vacío, establece el valor como null.
field.onChange(null);
} else {
const number = Number.parseInt(value, 10);
if (!Number.isNaN(number)) {
// Solo actualiza el valor si se convierte a un número válido.
field.onChange(number);
}
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,178 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { DatabaseBackup, Play } from "lucide-react";
import Link from "next/link";
import React from "react";
import { toast } from "sonner";
import { AddBackup } from "../../database/backups/add-backup";
import { DeleteBackup } from "../../database/backups/delete-backup";
import { UpdateBackup } from "../../database/backups/update-backup";
interface Props {
mysqlId: string;
}
export const ShowBackupMySql = ({ mysqlId }: Props) => {
const { data } = api.destination.all.useQuery();
const { data: mysql, refetch: refetchMySql } = api.mysql.one.useQuery(
{
mysqlId,
},
{
enabled: !!mysqlId,
},
);
const { mutateAsync: manualBackup, isLoading: isManualBackup } =
api.backup.manualBackupMySql.useMutation();
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between gap-4 flex-wrap">
<div className="flex flex-col gap-0.5">
<CardTitle className="text-xl">Backups</CardTitle>
<CardDescription>
Add backup to your database to save the data to a different
providers.
</CardDescription>
</div>
{mysql && mysql?.backups?.length > 0 && (
<AddBackup
databaseId={mysqlId}
databaseType="mysql"
refetch={refetchMySql}
/>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To create a backup is required to set at least 1 provider. Please,
go to{" "}
<Link
href="/dashboard/settings/server"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
) : (
<div>
{mysql?.backups.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No backups configured
</span>
<AddBackup
databaseId={mysqlId}
databaseType="mysql"
refetch={refetchMySql}
/>
</div>
) : (
<div className="flex flex-col pt-2">
<div className="flex flex-col gap-6">
{mysql?.backups.map((backup) => (
<div key={backup.backupId}>
<div className="flex w-full flex-col md:flex-row md:items-center justify-between gap-4 md:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 flex-col gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Destination</span>
<span className="text-sm text-muted-foreground">
{backup.destination.name}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Database</span>
<span className="text-sm text-muted-foreground">
{backup.database}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Scheduled</span>
<span className="text-sm text-muted-foreground">
{backup.schedule}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Prefix Storage</span>
<span className="text-sm text-muted-foreground">
{backup.prefix}
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium">Enabled</span>
<span className="text-sm text-muted-foreground">
{backup.enabled ? "Yes" : "No"}
</span>
</div>
</div>
<div className="flex flex-row gap-4">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
isLoading={isManualBackup}
onClick={async () => {
await manualBackup({
backupId: backup.backupId as string,
})
.then(async () => {
toast.success(
"Manual Backup Successful",
);
})
.catch(() => {
toast.error(
"Error to Create the manual backup",
);
});
}}
>
<Play className="size-5 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent>Run Manual Backup</TooltipContent>
</Tooltip>
</TooltipProvider>
<UpdateBackup
backupId={backup.backupId}
refetch={refetchMySql}
/>
<DeleteBackup
backupId={backup.backupId}
refetch={refetchMySql}
/>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,62 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
interface Props {
mysqlId: string;
}
export const DeleteMysql = ({ mysqlId }: Props) => {
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
const { push } = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mysqlId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database delete succesfully");
})
.catch(() => {
toast.error("Error to delete the database");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,140 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Toggle } from "@/components/ui/toggle";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addEnvironmentSchema = z.object({
environment: z.string(),
});
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
interface Props {
mysqlId: string;
}
export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
const [isEnvVisible, setIsEnvVisible] = useState(true);
const { mutateAsync, isLoading } = api.mysql.saveEnvironment.useMutation();
const { data, refetch } = api.mysql.one.useQuery(
{
mysqlId,
},
{
enabled: !!mysqlId,
},
);
const form = useForm<EnvironmentSchema>({
defaultValues: {
environment: "",
},
resolver: zodResolver(addEnvironmentSchema),
});
useEffect(() => {
if (data) {
form.reset({
environment: data.env || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: EnvironmentSchema) => {
mutateAsync({
env: data.environment,
mysqlId,
})
.then(async () => {
toast.success("Environments Added");
await refetch();
})
.catch(() => {
toast.error("Error to add environment");
});
};
return (
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader className="flex flex-row w-full items-center justify-between">
<div>
<CardTitle className="text-xl">Environment Settings</CardTitle>
<CardDescription>
You can add environment variables to your resource.
</CardDescription>
</div>
<Toggle
aria-label="Toggle bold"
pressed={isEnvVisible}
onPressedChange={setIsEnvVisible}
>
{isEnvVisible ? (
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
) : (
<EyeIcon className="h-4 w-4 text-muted-foreground" />
)}
</Toggle>
</CardHeader>
<CardContent>
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4"
>
<FormField
control={form.control}
name="environment"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
<CodeEditor
language="properties"
disabled={isEnvVisible}
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
);
};

View File

@@ -0,0 +1,73 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
mysqlId: string;
}
export const DeployMysql = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery(
{
mysqlId,
},
{ enabled: !!mysqlId },
);
const { mutateAsync: deploy } = api.mysql.deploy.useMutation();
const { mutateAsync: changeStatus } = api.mysql.changeStatus.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button isLoading={data?.applicationStatus === "running"}>
Deploy
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will deploy the mysql database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await changeStatus({
mysqlId,
applicationStatus: "running",
})
.then(async () => {
toast.success("Deploying Database....");
await refetch();
await deploy({
mysqlId,
}).catch(() => {
toast.error("Error to deploy Database");
});
await refetch();
})
.catch((e) => {
toast.error(e.message || "Error to deploy Database");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,69 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import { toast } from "sonner";
interface Props {
mysqlId: string;
appName: string;
}
export const ResetMysql = ({ mysqlId, appName }: Props) => {
const { refetch } = api.mysql.one.useQuery(
{
mysqlId,
},
{ enabled: !!mysqlId },
);
const { mutateAsync: reload, isLoading } = api.mysql.reload.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Reload
<RefreshCcw className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will reload the service
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await reload({
mysqlId,
appName,
})
.then(() => {
toast.success("Service Reloaded");
})
.catch(() => {
toast.error("Error to reload the service");
});
await refetch();
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,158 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => {
if (a !== null) {
const parsed = Number.parseInt(z.string().parse(a), 10);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}, z
.number()
.gte(0, "Range must be 0 - 65535")
.lte(65535, "Range must be 0 - 65535")
.nullable()),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
interface Props {
mysqlId: string;
}
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data: ip } = api.settings.getIp.useQuery();
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
const [connectionUrl, setConnectionUrl] = useState("");
const form = useForm<DockerProvider>({
defaultValues: {},
resolver: zodResolver(DockerProviderSchema),
});
useEffect(() => {
if (data?.externalPort) {
form.reset({
externalPort: data.externalPort,
});
}
}, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({
externalPort: values.externalPort,
mysqlId,
})
.then(async () => {
toast.success("External Port updated");
await refetch();
})
.catch(() => {
toast.error("Error to save the external port");
});
};
useEffect(() => {
const buildConnectionUrl = () => {
const port = form.watch("externalPort") || data?.externalPort;
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
};
setConnectionUrl(buildConnectionUrl());
}, [
data?.appName,
data?.externalPort,
data?.databasePassword,
data?.databaseName,
data?.databaseUser,
form,
ip,
]);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">External Credentials</CardTitle>
<CardDescription>
In order to make the database reachable trought internet is
required to set a port, make sure the port is not used by another
application or database
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid grid-cols-2 gap-4 ">
<div className="col-span-2 space-y-4">
<FormField
control={form.control}
name="externalPort"
render={({ field }) => {
return (
<FormItem>
<FormLabel>External Port (Internet)</FormLabel>
<FormControl>
<Input
placeholder="3306"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
</div>
{!!data?.externalPort && (
<div className="grid w-full gap-8">
<div className="flex flex-col gap-3">
<Label>External Host</Label>
<ToggleVisibilityInput disabled value={connectionUrl} />
</div>
</div>
)}
<div className="flex justify-end">
<Button type="submit" isLoading={isLoading}>
Save
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -0,0 +1,49 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { api } from "@/utils/api";
import { Terminal } from "lucide-react";
import React from "react";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { StartMysql } from "../start-mysql";
import { DeployMysql } from "./deploy-mysql";
import { ResetMysql } from "./reset-mysql";
import { StopMysql } from "./stop-mysql";
interface Props {
mysqlId: string;
}
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
const { data } = api.mysql.one.useQuery(
{
mysqlId,
},
{ enabled: !!mysqlId },
);
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<DeployMysql mysqlId={mysqlId} />
<ResetMysql mysqlId={mysqlId} appName={data?.appName || ""} />
{data?.applicationStatus === "idle" ? (
<StartMysql mysqlId={mysqlId} />
) : (
<StopMysql mysqlId={mysqlId} />
)}
<DockerTerminalModal appName={data?.appName || ""}>
<Button variant="outline">
<Terminal />
Open Terminal
</Button>
</DockerTerminalModal>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -0,0 +1,71 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import React from "react";
interface Props {
mysqlId: string;
}
export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
const { data } = api.mysql.one.useQuery({ mysqlId });
return (
<>
<div className="flex w-full flex-col gap-5 ">
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Internal Credentials</CardTitle>
</CardHeader>
<CardContent className="flex w-full flex-row gap-4">
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
<div className="flex flex-col gap-2">
<Label>User</Label>
<Input disabled value={data?.databaseUser} />
</div>
<div className="flex flex-col gap-2">
<Label>Database Name</Label>
<Input disabled value={data?.databaseName} />
</div>
<div className="flex flex-col gap-2">
<Label>Password</Label>
<div className="flex flex-row gap-4">
<ToggleVisibilityInput
disabled
value={data?.databasePassword}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Root Password</Label>
<div className="flex flex-row gap-4">
<ToggleVisibilityInput
disabled
value={data?.databaseRootPassword}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label>Internal Port (Container)</Label>
<Input disabled value="3306" />
</div>
<div className="flex flex-col gap-2">
<Label>Internal Host</Label>
<Input disabled value={data?.appName} />
</div>
<div className="flex flex-col gap-2 md:col-span-2">
<Label>Internal Connection URL </Label>
<ToggleVisibilityInput
disabled
value={`mysql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:3306/${data?.databaseName}`}
/>
</div>
</div>
</CardContent>
</Card>
</div>
</>
);
};

View File

@@ -0,0 +1,65 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Ban } from "lucide-react";
import { toast } from "sonner";
interface Props {
mysqlId: string;
}
export const StopMysql = ({ mysqlId }: Props) => {
const { mutateAsync, isLoading } = api.mysql.stop.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Stop
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure to stop the database?
</AlertDialogTitle>
<AlertDialogDescription>
This will stop the database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mysqlId,
})
.then(async () => {
await utils.mysql.one.invalidate({
mysqlId,
});
toast.success("Application stopped succesfully");
})
.catch(() => {
toast.error("Error to stop the Application");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,65 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
mysqlId: string;
}
export const StartMysql = ({ mysqlId }: Props) => {
const { mutateAsync, isLoading } = api.mysql.start.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Start
<CheckCircle2 className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to start the database?
</AlertDialogTitle>
<AlertDialogDescription>
This will start the database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mysqlId,
})
.then(async () => {
await utils.mysql.one.invalidate({
mysqlId,
});
toast.success("Database started succesfully");
})
.catch(() => {
toast.error("Error to start the Database");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,159 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, SquarePen } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const updateMysqlSchema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
});
type UpdateMysql = z.infer<typeof updateMysqlSchema>;
interface Props {
mysqlId: string;
}
export const UpdateMysql = ({ mysqlId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.mysql.update.useMutation();
const { data } = api.mysql.one.useQuery(
{
mysqlId,
},
{
enabled: !!mysqlId,
},
);
const form = useForm<UpdateMysql>({
defaultValues: {
description: data?.description ?? "",
name: data?.name ?? "",
},
resolver: zodResolver(updateMysqlSchema),
});
useEffect(() => {
if (data) {
form.reset({
description: data.description ?? "",
name: data.name,
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: UpdateMysql) => {
await mutateAsync({
name: formData.name,
mysqlId: mysqlId,
description: formData.description || "",
})
.then(() => {
toast.success("MySQL updated succesfully");
utils.mysql.one.invalidate({
mysqlId: mysqlId,
});
})
.catch(() => {
toast.error("Error to update the MySQL");
})
.finally(() => {});
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost">
<SquarePen className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Modify MySQL</DialogTitle>
<DialogDescription>Update the MySQL data</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid gap-4">
<div className="grid items-center gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-mysql-update"
className="grid w-full gap-4 "
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Tesla" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Description about your project..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-mysql-update"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,131 @@
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { AlertTriangle, Package } from "lucide-react";
import React from "react";
import { AddVolumes } from "../../application/advanced/volumes/add-volumes";
import { DeleteVolume } from "../../application/advanced/volumes/delete-volume";
import { UpdateVolume } from "../../application/advanced/volumes/update-volume";
interface Props {
mysqlId: string;
}
export const ShowVolumes = ({ mysqlId }: Props) => {
const { data, refetch } = api.mysql.one.useQuery(
{
mysqlId,
},
{ enabled: !!mysqlId },
);
return (
<Card className="bg-background">
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
<div>
<CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription>
If you want to persist data in this mysql use the following config
to setup the volumes
</CardDescription>
</div>
{data && data?.mounts.length > 0 && (
<AddVolumes serviceId={mysqlId} refetch={refetch} serviceType="mysql">
Add Volume
</AddVolumes>
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.mounts.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<Package className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No volumes/mounts configured
</span>
<AddVolumes
serviceId={mysqlId}
refetch={refetch}
serviceType="mysql"
>
Add Volume
</AddVolumes>
</div>
) : (
<div className="flex flex-col pt-2 gap-4">
<AlertBlock type="info">
Please remember to click Redeploy after adding, editing, or
deleting a mount to apply the changes.
</AlertBlock>
<div className="flex flex-col gap-6">
{data?.mounts.map((mount) => (
<div key={mount.mountId}>
<div
key={mount.mountId}
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground">
{mount.type.toUpperCase()}
</span>
</div>
{mount.type === "volume" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Volume Name</span>
<span className="text-sm text-muted-foreground">
{mount.volumeName}
</span>
</div>
)}
{mount.type === "file" && (
<>
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
{mount.content}
</span>
</div>
</>
)}
{mount.type === "bind" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Host Path</span>
<span className="text-sm text-muted-foreground">
{mount.hostPath}
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div>
<div className="flex flex-row gap-1">
<UpdateVolume
mountId={mount.mountId}
type={mount.type}
refetch={refetch}
serviceType="mysql"
/>
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
</div>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
};