mirror of
https://github.com/LukeHagar/dokploy.git
synced 2025-12-06 04:19:37 +00:00
Feat/add sidebar (#1084)
* refactor: add sidebar * chore: add deps * refactor: update sidebar * refactor: another layout * refactor: update variant * refactor: change layout * refactor: change variant * refactor: enhance sidebar navigation with active state management * feat: add project button to dashboard * Merge branch 'canary' into feat/add-sidebar * refactor: add loader * refactor: update destinations and refactor * refactor: ui refactor certificates * refactor: delete unused files * refactor: remove unused files and duplicate registry * refactor: update style registry * refactor: add new design registry * refactor: enhance git providers * refactor: remove duplicate files * refactor: update * refactor: update users * refactor: delete unused files * refactor: update profile * refactor: apply changes * refactor: update UI * refactor: enhance Docker monitoring UI layout * refactor: add theme toggle and language selection to user navigation (#1083) * refactor: remove unused files * feat: add filter to services * refactor: add active items * refactor: remove tab prop * refactor: remove unused files * refactor: remove duplicated files * refactor: remove unused files * refactor: remove duplicate files * refactor: remove unused files * refactor: delete unused files * refactor: remove unsued files * refactor: delete unused files * refactor: lint * refactor: remove unused secuirty * refactor: delete unused files * refactor: delete unused files * remove imports * refactor: add update button * refactor: delete unused files * refactor: remove unused code * refactor: remove unused files * refactor: update login page * refactor: update login UI * refactor: update ui reset password * refactor: add justify end * feat: add suscriptions * feat: add sheet * feat: add logs for postgres * feat: add logs for all databases * feat: add server logs with drawer logs * refactor: remove unused files * refactor: add refetch when closing * refactor: fix linter * chore: bump node-20 * revert * refactor: fix conflicts * refactor: update * refactor: add missing deps * refactor: delete duplicate files * refactor: delete unsued files * chore: lint * refactor: remove unsued file * refactor: add refetch * refactor: remove duplicated files * refactor: delete unused files * refactor: update setup onboarding * refactor: add breadcrumb * refactor: apply updates * refactor: add faker * refactor: use 0 in validation * refactor: show correct state * refactor: update --------- Co-authored-by: vishalkadam47 <vishal@jeevops.com> Co-authored-by: Vishal kadam <107353260+vishalkadam47@users.noreply.github.com>
This commit is contained in:
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
combine-manifests:
|
combine-manifests:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.0
|
- image: cimg/node:20.9.0
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- setup_remote_docker
|
- setup_remote_docker
|
||||||
@@ -99,14 +99,14 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- fix/nixpacks-version
|
- feat/add-sidebar
|
||||||
- build-arm64:
|
- build-arm64:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- fix/nixpacks-version
|
- feat/add-sidebar
|
||||||
- combine-manifests:
|
- combine-manifests:
|
||||||
requires:
|
requires:
|
||||||
- build-amd64
|
- build-amd64
|
||||||
@@ -116,4 +116,4 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- fix/nixpacks-version
|
- feat/add-sidebar
|
||||||
|
|||||||
6
.github/workflows/pull-request.yml
vendored
6
.github/workflows/pull-request.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.18.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.18.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.18.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ We have a few guidelines to follow when contributing to this project:
|
|||||||
|
|
||||||
## Commit Convention
|
## Commit Convention
|
||||||
|
|
||||||
|
|
||||||
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||||
|
|
||||||
### Commit Message Format
|
### Commit Message Format
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<type>[optional scope]: <description>
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
@@ -54,6 +52,8 @@ feat: add new feature
|
|||||||
|
|
||||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||||
|
|
||||||
|
We use Node v20.9.0
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dokploy/dokploy.git
|
git clone https://github.com/dokploy/dokploy.git
|
||||||
cd dokploy
|
cd dokploy
|
||||||
@@ -73,9 +73,10 @@ Run the command that will spin up all the required services and files.
|
|||||||
pnpm run dokploy:setup
|
pnpm run dokploy:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Run this script
|
Run this script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run server:script
|
pnpm run server:script
|
||||||
```
|
```
|
||||||
|
|
||||||
Now run the development server.
|
Now run the development server.
|
||||||
@@ -248,4 +249,3 @@ export function generate(schema: Schema): Template {
|
|||||||
## Docs & Website
|
## Docs & Website
|
||||||
|
|
||||||
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
To contribute to the Dokploy docs or website, please go to this [repository](https://github.com/Dokploy/website).
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-slim AS base
|
FROM node:20-slim AS base
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
@@ -7,7 +7,7 @@ FROM base AS build
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y python3 make g++ git python3-pip pkg-config libsecret-1-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
18.18.0
|
20.9.0
|
||||||
@@ -95,10 +95,10 @@ export const Login2FA = ({ authId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="pin"
|
name="pin"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col justify-center max-sm:items-center">
|
<FormItem className="flex flex-col max-sm:items-center">
|
||||||
<FormLabel>Pin</FormLabel>
|
<FormLabel>Pin</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex justify-center">
|
<div className="flex">
|
||||||
<InputOTP
|
<InputOTP
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
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 { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
portId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeletePort = ({ portId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { mutateAsync, isLoading } = api.port.delete.useMutation();
|
|
||||||
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 port
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
portId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
utils.application.one.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success("Port delete successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the port");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} 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 { PlusIcon } from "lucide-react";
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -45,18 +45,29 @@ type AddPort = z.infer<typeof AddPortSchema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
portId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddPort = ({
|
export const HandlePorts = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
|
portId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { data } = api.port.one.useQuery(
|
||||||
api.port.create.useMutation();
|
{
|
||||||
|
portId: portId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!portId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { mutateAsync, isLoading, error, isError } = portId
|
||||||
|
? api.port.update.useMutation()
|
||||||
|
: api.port.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddPort>({
|
const form = useForm<AddPort>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -68,32 +79,46 @@ export const AddPort = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
publishedPort: 0,
|
publishedPort: data?.publishedPort ?? 0,
|
||||||
targetPort: 0,
|
targetPort: data?.targetPort ?? 0,
|
||||||
|
protocol: data?.protocol ?? "tcp",
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddPort) => {
|
const onSubmit = async (data: AddPort) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
|
portId: portId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Port Created");
|
toast.success(portId ? "Port Updated" : "Port Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating the port");
|
toast.error(
|
||||||
|
portId ? "Error updating the port" : "Error creating the port",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
{portId ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button>{children}</Button>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -204,7 +229,7 @@ export const AddPort = ({
|
|||||||
form="hook-form-add-port"
|
form="hook-form-add-port"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{portId ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -7,23 +9,25 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Rss } from "lucide-react";
|
import { Rss, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddPort } from "./add-port";
|
import { toast } from "sonner";
|
||||||
import { DeletePort } from "./delete-port";
|
import { HandlePorts } from "./handle-ports";
|
||||||
import { UpdatePort } from "./update-port";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPorts = ({ applicationId }: Props) => {
|
export const ShowPorts = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deletePort, isLoading: isRemoving } =
|
||||||
|
api.port.delete.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -35,7 +39,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.ports.length > 0 && (
|
{data && data?.ports.length > 0 && (
|
||||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -45,7 +49,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No ports configured
|
No ports configured
|
||||||
</span>
|
</span>
|
||||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
@@ -78,8 +82,36 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<UpdatePort portId={port.portId} />
|
<HandlePorts
|
||||||
<DeletePort portId={port.portId} />
|
applicationId={applicationId}
|
||||||
|
portId={port.portId}
|
||||||
|
/>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Port"
|
||||||
|
description="Are you sure you want to delete this port?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deletePort({
|
||||||
|
portId: port.portId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Port deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting port");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
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, NumberInput } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const UpdatePortSchema = z.object({
|
|
||||||
publishedPort: z.number().int().min(1).max(65535),
|
|
||||||
targetPort: z.number().int().min(1).max(65535),
|
|
||||||
protocol: z.enum(["tcp", "udp"], {
|
|
||||||
required_error: "Protocol is required",
|
|
||||||
invalid_type_error: "Protocol must be a valid protocol",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdatePort = z.infer<typeof UpdatePortSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
portId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UpdatePort = ({ portId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { data } = api.port.one.useQuery(
|
|
||||||
{
|
|
||||||
portId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!portId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.port.update.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<UpdatePort>({
|
|
||||||
defaultValues: {},
|
|
||||||
resolver: zodResolver(UpdatePortSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
publishedPort: data.publishedPort,
|
|
||||||
targetPort: data.targetPort,
|
|
||||||
protocol: data.protocol,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: UpdatePort) => {
|
|
||||||
await mutateAsync({
|
|
||||||
portId,
|
|
||||||
publishedPort: data.publishedPort,
|
|
||||||
targetPort: data.targetPort,
|
|
||||||
protocol: data.protocol,
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
toast.success("Port Updated");
|
|
||||||
await utils.application.one.invalidate({
|
|
||||||
applicationId: response?.applicationId,
|
|
||||||
});
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the port");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Update</DialogTitle>
|
|
||||||
<DialogDescription>Update the port</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-update-redirect"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="publishedPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Published Port</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<NumberInput placeholder="1-65535" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="targetPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Target Port</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<NumberInput placeholder="1-65535" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="protocol"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem className="md:col-span-2">
|
|
||||||
<FormLabel>Protocol</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a protocol" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent defaultValue={"none"}>
|
|
||||||
<SelectItem value={"none"} disabled>
|
|
||||||
None
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={"tcp"}>TCP</SelectItem>
|
|
||||||
<SelectItem value={"udp"}>UDP</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-update-redirect"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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 { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
redirectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteRedirect = ({ redirectId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { mutateAsync, isLoading } = api.redirects.delete.useMutation();
|
|
||||||
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
|
|
||||||
redirect
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
redirectId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
utils.application.one.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Redirect delete successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the redirect");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -31,7 +31,7 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -77,19 +77,32 @@ const redirectPresets = [
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
redirectId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddRedirect = ({
|
export const HandleRedirect = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
|
redirectId,
|
||||||
children = <PlusIcon className="w-4 h-4" />,
|
children = <PlusIcon className="w-4 h-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [presetSelected, setPresetSelected] = useState("");
|
const [presetSelected, setPresetSelected] = useState("");
|
||||||
|
|
||||||
|
const { data, refetch } = api.redirects.one.useQuery(
|
||||||
|
{
|
||||||
|
redirectId: redirectId || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!redirectId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } = redirectId
|
||||||
api.redirects.create.useMutation();
|
? api.redirects.update.useMutation()
|
||||||
|
: api.redirects.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddRedirect>({
|
const form = useForm<AddRedirect>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -102,29 +115,35 @@ export const AddRedirect = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
permanent: false,
|
permanent: data?.permanent || false,
|
||||||
regex: "",
|
regex: data?.regex || "",
|
||||||
replacement: "",
|
replacement: data?.replacement || "",
|
||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddRedirect) => {
|
const onSubmit = async (data: AddRedirect) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
|
redirectId: redirectId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Redirect Created");
|
toast.success(redirectId ? "Redirect Updated" : "Redirect Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
|
refetch();
|
||||||
await utils.application.readTraefikConfig.invalidate({
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
onDialogToggle(false);
|
onDialogToggle(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating the redirect");
|
toast.error(
|
||||||
|
redirectId
|
||||||
|
? "Error updating the redirect"
|
||||||
|
: "Error creating the redirect",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,7 +167,17 @@ export const AddRedirect = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
{redirectId ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button>{children}</Button>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -243,7 +272,7 @@ export const AddRedirect = ({
|
|||||||
form="hook-form-add-redirect"
|
form="hook-form-add-redirect"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{redirectId ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -6,23 +8,28 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Split } from "lucide-react";
|
import { Split, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddRedirect } from "./add-redirect";
|
import { toast } from "sonner";
|
||||||
import { DeleteRedirect } from "./delete-redirect";
|
import { HandleRedirect } from "./handle-redirect";
|
||||||
import { UpdateRedirect } from "./update-redirect";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowRedirects = ({ applicationId }: Props) => {
|
export const ShowRedirects = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
|
||||||
|
api.redirects.delete.useMutation();
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -35,7 +42,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.redirects.length > 0 && (
|
{data && data?.redirects.length > 0 && (
|
||||||
<AddRedirect applicationId={applicationId}>Add Redirect</AddRedirect>
|
<HandleRedirect applicationId={applicationId}>
|
||||||
|
Add Redirect
|
||||||
|
</HandleRedirect>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -45,9 +54,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No redirects configured
|
No redirects configured
|
||||||
</span>
|
</span>
|
||||||
<AddRedirect applicationId={applicationId}>
|
<HandleRedirect applicationId={applicationId}>
|
||||||
Add Redirect
|
Add Redirect
|
||||||
</AddRedirect>
|
</HandleRedirect>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -76,8 +85,40 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<UpdateRedirect redirectId={redirect.redirectId} />
|
<HandleRedirect
|
||||||
<DeleteRedirect redirectId={redirect.redirectId} />
|
redirectId={redirect.redirectId}
|
||||||
|
applicationId={applicationId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Redirect"
|
||||||
|
description="Are you sure you want to delete this redirect?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteRedirect({
|
||||||
|
redirectId: redirect.redirectId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Redirect deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting redirect");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
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,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
const UpdateRedirectSchema = z.object({
|
|
||||||
regex: z.string().min(1, "Regex required"),
|
|
||||||
permanent: z.boolean().default(false),
|
|
||||||
replacement: z.string().min(1, "Replacement required"),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdateRedirect = z.infer<typeof UpdateRedirectSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
redirectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UpdateRedirect = ({ redirectId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { data } = api.redirects.one.useQuery(
|
|
||||||
{
|
|
||||||
redirectId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!redirectId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.redirects.update.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<UpdateRedirect>({
|
|
||||||
defaultValues: {
|
|
||||||
permanent: false,
|
|
||||||
regex: "",
|
|
||||||
replacement: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(UpdateRedirectSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
permanent: data.permanent || false,
|
|
||||||
regex: data.regex || "",
|
|
||||||
replacement: data.replacement || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateRedirect) => {
|
|
||||||
await mutateAsync({
|
|
||||||
redirectId,
|
|
||||||
permanent: data.permanent,
|
|
||||||
regex: data.regex,
|
|
||||||
replacement: data.replacement,
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
toast.success("Redirect Updated");
|
|
||||||
await utils.application.one.invalidate({
|
|
||||||
applicationId: response?.applicationId,
|
|
||||||
});
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the redirect");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Update</DialogTitle>
|
|
||||||
<DialogDescription>Update the redirect</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-update-redirect"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="regex"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Regex</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="^http://localhost/(.*)" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="replacement"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Replacement</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="http://mydomain/$${1}" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="permanent"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Permanent</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Set the permanent option to true to apply a permanent
|
|
||||||
redirection.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-update-redirect"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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 { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
securityId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteSecurity = ({ securityId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { mutateAsync, isLoading } = api.security.delete.useMutation();
|
|
||||||
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
|
|
||||||
security
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
securityId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
utils.application.one.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Security delete successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the security");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -35,17 +35,29 @@ type AddSecurity = z.infer<typeof AddSecuritychema>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
securityId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddSecurity = ({
|
export const HandleSecurity = ({
|
||||||
applicationId,
|
applicationId,
|
||||||
|
securityId,
|
||||||
children = <PlusIcon className="h-4 w-4" />,
|
children = <PlusIcon className="h-4 w-4" />,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { data } = api.security.one.useQuery(
|
||||||
api.security.create.useMutation();
|
{
|
||||||
|
securityId: securityId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!securityId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } = securityId
|
||||||
|
? api.security.update.useMutation()
|
||||||
|
: api.security.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddSecurity>({
|
const form = useForm<AddSecurity>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -56,16 +68,20 @@ export const AddSecurity = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset({
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
username: data?.username || "",
|
||||||
|
password: data?.password || "",
|
||||||
|
});
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddSecurity) => {
|
const onSubmit = async (data: AddSecurity) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
...data,
|
...data,
|
||||||
|
securityId: securityId || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Security Created");
|
toast.success(securityId ? "Security Updated" : "Security Created");
|
||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
@@ -75,20 +91,34 @@ export const AddSecurity = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating security");
|
toast.error(
|
||||||
|
securityId
|
||||||
|
? "Error updating the security"
|
||||||
|
: "Error creating security",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>{children}</Button>
|
{securityId ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button>{children}</Button>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Security</DialogTitle>
|
<DialogTitle>Security</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Add security to your application
|
{securityId ? "Update" : "Add"} security to your application
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
@@ -137,7 +167,7 @@ export const AddSecurity = ({
|
|||||||
form="hook-form-add-security"
|
form="hook-form-add-security"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
{securityId ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -6,23 +8,27 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { LockKeyhole } from "lucide-react";
|
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddSecurity } from "./add-security";
|
import { toast } from "sonner";
|
||||||
import { DeleteSecurity } from "./delete-security";
|
import { HandleSecurity } from "./handle-security";
|
||||||
import { UpdateSecurity } from "./update-security";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowSecurity = ({ applicationId }: Props) => {
|
export const ShowSecurity = ({ applicationId }: Props) => {
|
||||||
const { data } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
|
||||||
|
api.security.delete.useMutation();
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -32,7 +38,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.security.length > 0 && (
|
{data && data?.security.length > 0 && (
|
||||||
<AddSecurity applicationId={applicationId}>Add Security</AddSecurity>
|
<HandleSecurity applicationId={applicationId}>
|
||||||
|
Add Security
|
||||||
|
</HandleSecurity>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -42,9 +50,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No security configured
|
No security configured
|
||||||
</span>
|
</span>
|
||||||
<AddSecurity applicationId={applicationId}>
|
<HandleSecurity applicationId={applicationId}>
|
||||||
Add Security
|
Add Security
|
||||||
</AddSecurity>
|
</HandleSecurity>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -67,8 +75,39 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<UpdateSecurity securityId={security.securityId} />
|
<HandleSecurity
|
||||||
<DeleteSecurity securityId={security.securityId} />
|
securityId={security.securityId}
|
||||||
|
applicationId={applicationId}
|
||||||
|
/>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Security"
|
||||||
|
description="Are you sure you want to delete this security?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteSecurity({
|
||||||
|
securityId: security.securityId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId,
|
||||||
|
});
|
||||||
|
toast.success("Security deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting security");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
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 { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const UpdateSecuritySchema = z.object({
|
|
||||||
username: z.string().min(1, "Username is required"),
|
|
||||||
password: z.string().min(1, "Password is required"),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdateSecurity = z.infer<typeof UpdateSecuritySchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
securityId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UpdateSecurity = ({ securityId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { data } = api.security.one.useQuery(
|
|
||||||
{
|
|
||||||
securityId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!securityId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
|
||||||
api.security.update.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<UpdateSecurity>({
|
|
||||||
defaultValues: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(UpdateSecuritySchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
username: data.username || "",
|
|
||||||
password: data.password || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: UpdateSecurity) => {
|
|
||||||
await mutateAsync({
|
|
||||||
securityId,
|
|
||||||
username: data.username,
|
|
||||||
password: data.password,
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
toast.success("Security Updated");
|
|
||||||
await utils.application.one.invalidate({
|
|
||||||
applicationId: response?.applicationId,
|
|
||||||
});
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the security");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Update</DialogTitle>
|
|
||||||
<DialogDescription>Update the security</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-update-security"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4 "
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="test1" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="test" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-update-security"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
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";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
|
|
||||||
const addResourcesApplication = z.object({
|
|
||||||
memoryReservation: z.number().nullable().optional(),
|
|
||||||
cpuLimit: z.number().nullable().optional(),
|
|
||||||
memoryLimit: z.number().nullable().optional(),
|
|
||||||
cpuReservation: z.number().nullable().optional(),
|
|
||||||
});
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddResourcesApplication = z.infer<typeof addResourcesApplication>;
|
|
||||||
|
|
||||||
export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
|
||||||
{
|
|
||||||
applicationId,
|
|
||||||
},
|
|
||||||
{ enabled: !!applicationId },
|
|
||||||
);
|
|
||||||
const { mutateAsync, isLoading } = api.application.update.useMutation();
|
|
||||||
const form = useForm<AddResourcesApplication>({
|
|
||||||
defaultValues: {},
|
|
||||||
resolver: zodResolver(addResourcesApplication),
|
|
||||||
});
|
|
||||||
|
|
||||||
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: AddResourcesApplication) => {
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
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 updating 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>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory soft limit in bytes. Example: 256MB =
|
|
||||||
268435456 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="268435456 (256MB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="memoryLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory hard limit in bytes. Example: 1GB =
|
|
||||||
1073741824 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1073741824 (1GB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
|
||||||
CPUs = 2000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="2000000000 (2 CPUs)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuReservation"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU shares (relative weight). Example: 1 CPU =
|
|
||||||
1000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1000000000 (1 CPU)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -16,42 +16,78 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
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 {
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
TooltipContent,
|
|
||||||
Tooltip,
|
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const addResourcesMysql = z.object({
|
const addResourcesSchema = z.object({
|
||||||
memoryReservation: z.number().nullable().optional(),
|
memoryReservation: z.string().optional(),
|
||||||
cpuLimit: z.number().nullable().optional(),
|
cpuLimit: z.string().optional(),
|
||||||
memoryLimit: z.number().nullable().optional(),
|
memoryLimit: z.string().optional(),
|
||||||
cpuReservation: z.number().nullable().optional(),
|
cpuReservation: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type ServiceType =
|
||||||
|
| "postgres"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "mysql"
|
||||||
|
| "mariadb"
|
||||||
|
| "application";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
id: string;
|
||||||
|
type: ServiceType | "application";
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddResourcesMysql = z.infer<typeof addResourcesMysql>;
|
type AddResources = z.infer<typeof addResourcesSchema>;
|
||||||
export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
export const ShowResources = ({ id, type }: Props) => {
|
||||||
const { data, refetch } = api.mysql.one.useQuery(
|
const queryMap = {
|
||||||
{
|
postgres: () =>
|
||||||
mysqlId,
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<AddResources>({
|
||||||
|
defaultValues: {
|
||||||
|
cpuLimit: "",
|
||||||
|
cpuReservation: "",
|
||||||
|
memoryLimit: "",
|
||||||
|
memoryReservation: "",
|
||||||
},
|
},
|
||||||
{ enabled: !!mysqlId },
|
resolver: zodResolver(addResourcesSchema),
|
||||||
);
|
|
||||||
const { mutateAsync, isLoading } = api.mysql.update.useMutation();
|
|
||||||
const form = useForm<AddResourcesMysql>({
|
|
||||||
defaultValues: {},
|
|
||||||
resolver: zodResolver(addResourcesMysql),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -65,9 +101,14 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [data, form, form.reset]);
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: AddResourcesMysql) => {
|
const onSubmit = async (formData: AddResources) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
mysqlId,
|
mongoId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
applicationId: id || "",
|
||||||
cpuLimit: formData.cpuLimit || null,
|
cpuLimit: formData.cpuLimit || null,
|
||||||
cpuReservation: formData.cpuReservation || null,
|
cpuReservation: formData.cpuReservation || null,
|
||||||
memoryLimit: formData.memoryLimit || null,
|
memoryLimit: formData.memoryLimit || null,
|
||||||
@@ -81,6 +122,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
toast.error("Error updating the resources");
|
toast.error("Error updating the resources");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -127,18 +169,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="268435456 (256MB in bytes)"
|
placeholder="268435456 (256MB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -172,18 +202,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="1073741824 (1GB in bytes)"
|
placeholder="1073741824 (1GB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -219,17 +237,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
placeholder="2000000000 (2 CPUs)"
|
placeholder="2000000000 (2 CPUs)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -260,22 +267,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="1000000000 (1 CPU)" {...field} />
|
||||||
placeholder="1000000000 (1 CPU)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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 React from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
mountId: string;
|
|
||||||
refetch: () => void;
|
|
||||||
}
|
|
||||||
export const DeleteVolume = ({ mountId, refetch }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.mounts.remove.useMutation();
|
|
||||||
|
|
||||||
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 mount
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
mountId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Mount deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the mount");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -7,40 +9,49 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Package } from "lucide-react";
|
import { Package, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { ServiceType } from "../show-resources";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
import { DeleteVolume } from "./delete-volume";
|
|
||||||
import { UpdateVolume } from "./update-volume";
|
import { UpdateVolume } from "./update-volume";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: ServiceType | "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowVolumes = ({ applicationId }: Props) => {
|
export const ShowVolumes = ({ id, type }: Props) => {
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const queryMap = {
|
||||||
{
|
postgres: () =>
|
||||||
applicationId,
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
},
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
{ enabled: !!applicationId },
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
);
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
compose: () =>
|
||||||
|
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
const { mutateAsync: deleteVolume, isLoading: isRemoving } =
|
||||||
|
api.mounts.remove.useMutation();
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Volumes</CardTitle>
|
<CardTitle className="text-xl">Volumes</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
If you want to persist data in this application use the following
|
If you want to persist data in this postgres database use the
|
||||||
config to setup the volumes
|
following config to setup the volumes
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
{data && data?.mounts.length > 0 && (
|
||||||
<AddVolumes
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
serviceId={applicationId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="application"
|
|
||||||
>
|
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
)}
|
)}
|
||||||
@@ -52,17 +63,13 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No volumes/mounts configured
|
No volumes/mounts configured
|
||||||
</span>
|
</span>
|
||||||
<AddVolumes
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
serviceId={applicationId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="application"
|
|
||||||
>
|
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
<AlertBlock type="info">
|
<AlertBlock type="warning">
|
||||||
Please remember to click Redeploy after adding, editing, or
|
Please remember to click Redeploy after adding, editing, or
|
||||||
deleting a mount to apply the changes.
|
deleting a mount to apply the changes.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
@@ -73,7 +80,8 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
key={mount.mountId}
|
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"
|
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-4 flex-col gap-4 sm:gap-8">
|
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
|
||||||
|
<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">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Mount Type</span>
|
<span className="font-medium">Mount Type</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -90,21 +98,12 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<>
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<span className="font-medium">Content</span>
|
||||||
<span className="font-medium">Content</span>
|
<span className="text-sm text-muted-foreground">
|
||||||
<span className="text-sm text-muted-foreground">
|
{mount.content}
|
||||||
{mount.content}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="font-medium">File Path</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{mount.filePath}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{mount.type === "bind" && (
|
{mount.type === "bind" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -126,9 +125,34 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
serviceType="application"
|
serviceType={type}
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DialogAction
|
||||||
|
title="Delete Volume"
|
||||||
|
description="Are you sure you want to delete this volume?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteVolume({
|
||||||
|
mountId: mount.mountId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Volume deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting volume");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Pencil } from "lucide-react";
|
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -177,8 +177,13 @@ export const UpdateVolume = ({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
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 { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Copy, TrashIcon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const deleteApplicationSchema = z.object({
|
|
||||||
projectName: z.string().min(1, {
|
|
||||||
message: "Application name is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteApplication = ({ applicationId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, isLoading } = api.application.delete.useMutation();
|
|
||||||
const { data } = api.application.one.useQuery(
|
|
||||||
{ applicationId },
|
|
||||||
{ enabled: !!applicationId },
|
|
||||||
);
|
|
||||||
const { push } = useRouter();
|
|
||||||
const form = useForm<DeleteApplication>({
|
|
||||||
defaultValues: {
|
|
||||||
projectName: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(deleteApplicationSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: DeleteApplication) => {
|
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
|
||||||
if (formData.projectName === expectedName) {
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
|
||||||
toast.success("Application deleted successfully");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the application");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.setError("projectName", {
|
|
||||||
message: "Project name does not match",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
This action cannot be undone. This will permanently delete the
|
|
||||||
application. If you are sure please enter the application name to
|
|
||||||
delete this application.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-delete-application"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="projectName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
To confirm, type{" "}
|
|
||||||
<Badge
|
|
||||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.name && data?.appName) {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${data.name}/${data.appName}`,
|
|
||||||
);
|
|
||||||
toast.success("Copied to clipboard. Be careful!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data?.name}/{data?.appName}
|
|
||||||
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
|
||||||
</Badge>{" "}
|
|
||||||
in the box below:
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter application name to confirm"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-delete-application"
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -20,6 +20,12 @@ interface Props {
|
|||||||
|
|
||||||
export const CancelQueues = ({ applicationId }: Props) => {
|
export const CancelQueues = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
|
if (isCloud) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ export const AddDomain = ({
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId
|
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||||
? "Error updating the domain"
|
|
||||||
: "Error creating the domain",
|
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
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 React from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
domainId: string;
|
|
||||||
}
|
|
||||||
export const DeleteDomain = ({ domainId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.domain.delete.useMutation();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
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
|
|
||||||
domain
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
domainId,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
if (data?.applicationId) {
|
|
||||||
utils.domain.byApplicationId.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId: data?.applicationId,
|
|
||||||
});
|
|
||||||
} else if (data?.composeId) {
|
|
||||||
utils.domain.byComposeId.invalidate({
|
|
||||||
composeId: data?.composeId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("Domain delete successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the Domain");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,17 +9,17 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { AddDomain } from "./add-domain";
|
import { AddDomain } from "./add-domain";
|
||||||
import { DeleteDomain } from "./delete-domain";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ applicationId }: Props) => {
|
export const ShowDomains = ({ applicationId }: Props) => {
|
||||||
const { data } = api.domain.byApplicationId.useQuery(
|
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
@@ -26,6 +27,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||||
|
api.domain.delete.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -97,7 +102,32 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AddDomain>
|
</AddDomain>
|
||||||
<DeleteDomain domainId={item.domainId} />
|
<DialogAction
|
||||||
|
title="Delete Domain"
|
||||||
|
description="Are you sure you want to delete this domain?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteDomain({
|
||||||
|
domainId: item.domainId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Domain deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting domain");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ import { Toggle } from "@/components/ui/toggle";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { type CSSProperties, useEffect, useState } from "react";
|
||||||
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";
|
||||||
|
import type { ServiceType } from "../advanced/show-resources";
|
||||||
|
|
||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@@ -30,21 +31,39 @@ const addEnvironmentSchema = z.object({
|
|||||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
id: string;
|
||||||
|
type: Exclude<ServiceType | "compose", "application">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
export const ShowEnvironment = ({ id, type }: Props) => {
|
||||||
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
compose: () =>
|
||||||
|
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveEnvironment.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
const mutationMap = {
|
||||||
{
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
mongoId,
|
redis: () => api.redis.update.useMutation(),
|
||||||
},
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
{
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
enabled: !!mongoId,
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
},
|
compose: () => api.compose.update.useMutation(),
|
||||||
);
|
};
|
||||||
|
const { mutateAsync, isLoading } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<EnvironmentSchema>({
|
const form = useForm<EnvironmentSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
environment: "",
|
environment: "",
|
||||||
@@ -62,8 +81,13 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (data: EnvironmentSchema) => {
|
const onSubmit = async (data: EnvironmentSchema) => {
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
|
mongoId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
env: data.environment,
|
env: data.environment,
|
||||||
mongoId,
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Environments Added");
|
toast.success("Environments Added");
|
||||||
@@ -111,6 +135,11 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
|||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
WebkitTextSecurity: isEnvVisible ? "disc" : null,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
language="properties"
|
language="properties"
|
||||||
disabled={isEnvVisible}
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
@@ -55,7 +55,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background px-6 pb-6">
|
<Card className="bg-background px-6 pb-6">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -97,5 +97,5 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
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 { useRouter } from "next/router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeployApplication = ({ applicationId }: Props) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
|
||||||
{
|
|
||||||
applicationId,
|
|
||||||
},
|
|
||||||
{ enabled: !!applicationId },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: deploy } = api.application.deploy.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 application
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await deploy({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Application deployed successfully");
|
|
||||||
await refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying the Application");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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 {
|
|
||||||
applicationId: string;
|
|
||||||
appName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ResetApplication = ({ applicationId, appName }: Props) => {
|
|
||||||
const { refetch } = api.application.one.useQuery(
|
|
||||||
{
|
|
||||||
applicationId,
|
|
||||||
},
|
|
||||||
{ enabled: !!applicationId },
|
|
||||||
);
|
|
||||||
const { mutateAsync: reload, isLoading } =
|
|
||||||
api.application.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 application
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
applicationId,
|
|
||||||
appName,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Service Reloaded");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading the service");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
||||||
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
import { RedbuildApplication } from "../rebuild-application";
|
|
||||||
import { StartApplication } from "../start-application";
|
|
||||||
import { StopApplication } from "../stop-application";
|
|
||||||
import { DeployApplication } from "./deploy-application";
|
|
||||||
import { ResetApplication } from "./reset-application";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -25,6 +23,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: update } = api.application.update.useMutation();
|
const { mutateAsync: update } = api.application.update.useMutation();
|
||||||
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
|
api.application.start.useMutation();
|
||||||
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
|
api.application.stop.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: deploy, isLoading: isDeploying } =
|
||||||
|
api.application.deploy.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
|
api.application.reload.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: redeploy } = api.application.redeploy.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -33,17 +43,127 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DeployApplication applicationId={applicationId} />
|
<DialogAction
|
||||||
<ResetApplication
|
title="Deploy Application"
|
||||||
applicationId={applicationId}
|
description="Are you sure you want to deploy this application?"
|
||||||
appName={data?.appName || ""}
|
type="default"
|
||||||
/>
|
onClick={async () => {
|
||||||
|
await deploy({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application deployed successfully");
|
||||||
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deploying application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Application"
|
||||||
|
description="Are you sure you want to reload this application?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
applicationId: applicationId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isReloading}>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Rebuild Application"
|
||||||
|
description="Are you sure you want to rebuild this application?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application rebuilt successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error rebuilding application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
<RedbuildApplication applicationId={applicationId} />
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<StartApplication applicationId={applicationId} />
|
<DialogAction
|
||||||
|
title="Start Application"
|
||||||
|
description="Are you sure you want to start this application?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isStarting}>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<StopApplication applicationId={applicationId} />
|
<DialogAction
|
||||||
|
title="Stop Application"
|
||||||
|
description="Are you sure you want to stop this application?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
applicationId: applicationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="destructive" isLoading={isStopping}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ export const AddPreviewDomain = ({
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId
|
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||||
? "Error updating the domain"
|
|
||||||
: "Error creating the domain",
|
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
|
|||||||
@@ -291,16 +291,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
"PORT=3000",
|
"PORT=3000",
|
||||||
].join("\n")}
|
].join("\n")}
|
||||||
/>
|
/>
|
||||||
{/* <CodeEditor
|
|
||||||
lineWrapping
|
|
||||||
language="properties"
|
|
||||||
wrapperClassName="h-[25rem] font-mono"
|
|
||||||
placeholder={`NODE_ENV=production
|
|
||||||
PORT=3000
|
|
||||||
|
|
||||||
`}
|
|
||||||
{...field}
|
|
||||||
/> */}
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
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 { Hammer } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RedbuildApplication = ({ applicationId }: Props) => {
|
|
||||||
const { data } = api.application.one.useQuery(
|
|
||||||
{
|
|
||||||
applicationId,
|
|
||||||
},
|
|
||||||
{ enabled: !!applicationId },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync } = api.application.redeploy.useMutation();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
>
|
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
Are you sure to rebuild the application?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
Is required to deploy at least 1 time in order to reuse the same
|
|
||||||
code
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
toast.success("Redeploying Application....");
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.application.one.invalidate({
|
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error rebuilding the application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StartApplication = ({ applicationId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.application.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 application?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will start the application
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.application.one.invalidate({
|
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Application started successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting the Application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
applicationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StopApplication = ({ applicationId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.application.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 application?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will stop the application
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
applicationId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.application.one.invalidate({
|
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Application stopped successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping the Application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -91,8 +91,12 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { 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 {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowVolumesCompose = ({ composeId }: Props) => {
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{ enabled: !!composeId },
|
|
||||||
);
|
|
||||||
|
|
||||||
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 compose use the following config
|
|
||||||
to setup the volumes
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
|
||||||
<AddVolumes
|
|
||||||
serviceId={composeId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="compose"
|
|
||||||
>
|
|
||||||
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={composeId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="compose"
|
|
||||||
>
|
|
||||||
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-4 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 w-40 truncate">
|
|
||||||
{mount.content}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="font-medium">File Path</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{mount.filePath}
|
|
||||||
</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="compose"
|
|
||||||
/>
|
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -22,7 +21,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
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 "lucide-react";
|
import { Copy, Trash2 } from "lucide-react";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -82,8 +81,13 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
<Button
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ interface Props {
|
|||||||
|
|
||||||
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
|
if (isCloud) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
|
|||||||
@@ -126,9 +126,7 @@ export const AddDomainCompose = ({
|
|||||||
|
|
||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId
|
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||||
? "Error updating the domain"
|
|
||||||
: "Error creating the domain",
|
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,9 +9,9 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
|
import { ExternalLink, GlobeIcon, PenBoxIcon, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { DeleteDomain } from "../../application/domains/delete-domain";
|
import { toast } from "sonner";
|
||||||
import { AddDomainCompose } from "./add-domain";
|
import { AddDomainCompose } from "./add-domain";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -18,7 +19,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||||
const { data } = api.domain.byComposeId.useQuery(
|
const { data, refetch } = api.domain.byComposeId.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
},
|
},
|
||||||
@@ -27,6 +28,9 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||||
|
api.domain.delete.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -97,7 +101,32 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</AddDomainCompose>
|
</AddDomainCompose>
|
||||||
<DeleteDomain domainId={item.domainId} />
|
<DialogAction
|
||||||
|
title="Delete Domain"
|
||||||
|
description="Are you sure you want to delete this domain?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteDomain({
|
||||||
|
domainId: item.domainId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Domain deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting domain");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
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 {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
|
||||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
|
||||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!composeId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Environments Added");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error adding environment");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEnvVisible) {
|
|
||||||
if (data?.env) {
|
|
||||||
const maskedLines = data.env
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => "*".repeat(line.length))
|
|
||||||
.join("\n");
|
|
||||||
form.reset({
|
|
||||||
environment: maskedLines,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.reset({
|
|
||||||
environment: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
form.reset({
|
|
||||||
environment: data?.env || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form.reset, data, form, isEnvVisible]);
|
|
||||||
|
|
||||||
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
|
|
||||||
disabled={isEnvVisible}
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="w-fit"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +1,17 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react";
|
||||||
import Link from "next/link";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
import { StartCompose } from "../start-compose";
|
|
||||||
import { DeployCompose } from "./deploy-compose";
|
|
||||||
import { RedbuildCompose } from "./rebuild-compose";
|
|
||||||
import { StopCompose } from "./stop-compose";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
}
|
}
|
||||||
export const ComposeActions = ({ composeId }: Props) => {
|
export const ComposeActions = ({ composeId }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
@@ -30,33 +19,109 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: update } = api.compose.update.useMutation();
|
const { mutateAsync: update } = api.compose.update.useMutation();
|
||||||
|
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||||
const extractDomains = (env: string) => {
|
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
|
||||||
const lines = env.split("\n");
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
const hostLines = lines.filter((line) => {
|
api.compose.start.useMutation();
|
||||||
const [key, value] = line.split("=");
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
return key?.trim().endsWith("_HOST");
|
api.compose.stop.useMutation();
|
||||||
});
|
|
||||||
|
|
||||||
const hosts = hostLines.map((line) => {
|
|
||||||
const [key, value] = line.split("=");
|
|
||||||
return value ? value.trim() : "";
|
|
||||||
});
|
|
||||||
|
|
||||||
return hosts;
|
|
||||||
};
|
|
||||||
|
|
||||||
const domains = extractDomains(data?.env || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<DeployCompose composeId={composeId} />
|
<DialogAction
|
||||||
<RedbuildCompose composeId={composeId} />
|
title="Deploy Compose"
|
||||||
|
description="Are you sure you want to deploy this compose?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await deploy({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose deployed successfully");
|
||||||
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deploying compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="default" isLoading={data?.composeStatus === "running"}>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Rebuild Compose"
|
||||||
|
description="Are you sure you want to rebuild this compose?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose rebuilt successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error rebuilding compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
>
|
||||||
|
Rebuild
|
||||||
|
<Hammer className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
{data?.composeType === "docker-compose" &&
|
{data?.composeType === "docker-compose" &&
|
||||||
data?.composeStatus === "idle" ? (
|
data?.composeStatus === "idle" ? (
|
||||||
<StartCompose composeId={composeId} />
|
<DialogAction
|
||||||
|
title="Start Compose"
|
||||||
|
description="Are you sure you want to start this compose?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isStarting}>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<StopCompose composeId={composeId} />
|
<DialogAction
|
||||||
|
title="Stop Compose"
|
||||||
|
description="Are you sure you want to stop this compose?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="destructive" isLoading={isStopping}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
@@ -89,41 +154,6 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{domains.length > 0 && (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline">
|
|
||||||
Domains
|
|
||||||
<Globe className="text-xs size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="w-56">
|
|
||||||
<DropdownMenuLabel>Domains detected</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
{domains.map((host, index) => {
|
|
||||||
const url =
|
|
||||||
host.startsWith("http://") || host.startsWith("https://")
|
|
||||||
? host
|
|
||||||
: `http://${host}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={`domain-${index}`}
|
|
||||||
className="cursor-pointer"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link href={url} target="_blank">
|
|
||||||
{host}
|
|
||||||
<ExternalLink className="ml-2 text-xs text-muted-foreground" />
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
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 { useRouter } from "next/router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeployCompose = ({ composeId }: Props) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{ enabled: !!composeId },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button isLoading={data?.composeStatus === "running"}>Deploy</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will deploy the compose
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
toast.success("Deploying Compose....");
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
await deploy({
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying Compose");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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 { Hammer } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RedbuildCompose = ({ composeId }: Props) => {
|
|
||||||
const { data } = api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{ enabled: !!composeId },
|
|
||||||
);
|
|
||||||
const { mutateAsync } = api.compose.redeploy.useMutation();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.composeStatus === "running"}
|
|
||||||
>
|
|
||||||
Rebuild
|
|
||||||
<Hammer className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
Are you sure to rebuild the compose?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
Is required to deploy at least 1 time in order to reuse the same
|
|
||||||
code
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
toast.success("Redeploying Compose....");
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.compose.one.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error rebuilding the compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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 {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StopCompose = ({ composeId }: Props) => {
|
|
||||||
const { data } = api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{ enabled: !!composeId },
|
|
||||||
);
|
|
||||||
const { mutateAsync, isLoading } = api.compose.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 sure to stop the compose?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will stop the compose services
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.compose.one.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
toast.success("Compose stopped successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping the compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StartCompose = ({ composeId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.compose.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 compose?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will start the compose
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.compose.one.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
toast.success("Compose started successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting the Compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StopCompose = ({ composeId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.compose.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 compose?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will stop the compose
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.compose.one.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
toast.success("Compose stopped successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping the Compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { SquarePen } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -91,8 +91,12 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
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 { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
backupId: string;
|
|
||||||
refetch: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteBackup = ({ backupId, refetch }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.backup.remove.useMutation();
|
|
||||||
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
|
|
||||||
backup
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
backupId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
|
|
||||||
toast.success("Backup deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the backup");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -13,31 +14,47 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { DatabaseBackup, Play } from "lucide-react";
|
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddBackup } from "../../database/backups/add-backup";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { DeleteBackup } from "../../database/backups/delete-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
import { UpdateBackup } from "../../database/backups/update-backup";
|
import { UpdateBackup } from "./update-backup";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
id: string;
|
||||||
|
type: Exclude<ServiceType, "application" | "redis">;
|
||||||
}
|
}
|
||||||
|
export const ShowBackups = ({ id, type }: Props) => {
|
||||||
export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
const queryMap = {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
const { data } = api.destination.all.useQuery();
|
const { data } = api.destination.all.useQuery();
|
||||||
const { data: postgres, refetch: refetchPostgres } =
|
const { data: postgres, refetch } = queryMap[type]
|
||||||
api.postgres.one.useQuery(
|
? queryMap[type]()
|
||||||
{
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
postgresId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!postgresId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } =
|
const mutationMap = {
|
||||||
api.backup.manualBackupPostgres.useMutation();
|
postgres: () => api.backup.manualBackupPostgres.useMutation(),
|
||||||
|
mysql: () => api.backup.manualBackupMySql.useMutation(),
|
||||||
|
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
|
||||||
|
mongo: () => api.backup.manualBackupMongo.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
|
||||||
|
type
|
||||||
|
]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.backup.manualBackupMongo.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: deleteBackup, isLoading: isRemoving } =
|
||||||
|
api.backup.remove.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -51,11 +68,7 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
<AddBackup
|
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
||||||
databaseId={postgresId}
|
|
||||||
databaseType="postgres"
|
|
||||||
refetch={refetchPostgres}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -83,9 +96,9 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
|||||||
No backups configured
|
No backups configured
|
||||||
</span>
|
</span>
|
||||||
<AddBackup
|
<AddBackup
|
||||||
databaseId={postgresId}
|
databaseId={id}
|
||||||
databaseType="postgres"
|
databaseType={type}
|
||||||
refetch={refetchPostgres}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -158,12 +171,34 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<UpdateBackup
|
<UpdateBackup
|
||||||
backupId={backup.backupId}
|
backupId={backup.backupId}
|
||||||
refetch={refetchPostgres}
|
refetch={refetch}
|
||||||
/>
|
|
||||||
<DeleteBackup
|
|
||||||
backupId={backup.backupId}
|
|
||||||
refetch={refetchPostgres}
|
|
||||||
/>
|
/>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Backup"
|
||||||
|
description="Are you sure you want to delete this backup?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteBackup({
|
||||||
|
backupId: backup.backupId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Backup deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting backup");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,8 +116,12 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button
|
||||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -9,10 +9,18 @@ import {
|
|||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown, Container } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
@@ -71,139 +79,164 @@ export const ShowContainers = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 grid gap-4 pb-20 w-full">
|
<div className="w-full">
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto">
|
||||||
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
<Input
|
<CardHeader className="">
|
||||||
placeholder="Filter by name..."
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
<Container className="size-6 text-muted-foreground self-center" />
|
||||||
onChange={(event) =>
|
Docker Containers
|
||||||
table.getColumn("name")?.setFilterValue(event.target.value)
|
</CardTitle>
|
||||||
}
|
<CardDescription>
|
||||||
className="md:max-w-sm"
|
See all the containers of your dokploy server
|
||||||
/>
|
</CardDescription>
|
||||||
<DropdownMenu>
|
</CardHeader>
|
||||||
<DropdownMenuTrigger asChild>
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
<Button variant="outline" className="sm:ml-auto max-sm:w-full">
|
<div className="gap-4 pb-20 w-full">
|
||||||
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||||
</Button>
|
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||||
</DropdownMenuTrigger>
|
<Input
|
||||||
<DropdownMenuContent align="end">
|
placeholder="Filter by name..."
|
||||||
{table
|
value={
|
||||||
.getAllColumns()
|
(table.getColumn("name")?.getFilterValue() as string) ??
|
||||||
.filter((column) => column.getCanHide())
|
""
|
||||||
.map((column) => {
|
}
|
||||||
return (
|
onChange={(event) =>
|
||||||
<DropdownMenuCheckboxItem
|
table
|
||||||
key={column.id}
|
.getColumn("name")
|
||||||
className="capitalize"
|
?.setFilterValue(event.target.value)
|
||||||
checked={column.getIsVisible()}
|
}
|
||||||
onCheckedChange={(value) =>
|
className="md:max-w-sm"
|
||||||
column.toggleVisibility(!!value)
|
/>
|
||||||
}
|
<DropdownMenu>
|
||||||
>
|
<DropdownMenuTrigger asChild>
|
||||||
{column.id}
|
<Button
|
||||||
</DropdownMenuCheckboxItem>
|
variant="outline"
|
||||||
);
|
className="sm:ml-auto max-sm:w-full"
|
||||||
})}
|
>
|
||||||
</DropdownMenuContent>
|
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||||
</DropdownMenu>
|
</Button>
|
||||||
</div>
|
</DropdownMenuTrigger>
|
||||||
<div className="rounded-md border">
|
<DropdownMenuContent align="end">
|
||||||
{isLoading ? (
|
{table
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
.getAllColumns()
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
.filter((column) => column.getCanHide())
|
||||||
Loading...
|
.map((column) => {
|
||||||
</span>
|
return (
|
||||||
</div>
|
<DropdownMenuCheckboxItem
|
||||||
) : data?.length === 0 ? (
|
key={column.id}
|
||||||
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
|
className="capitalize"
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
checked={column.getIsVisible()}
|
||||||
No results.
|
onCheckedChange={(value) =>
|
||||||
</span>
|
column.toggleVisibility(!!value)
|
||||||
</div>
|
}
|
||||||
) : (
|
>
|
||||||
<Table>
|
{column.id}
|
||||||
<TableHeader>
|
</DropdownMenuCheckboxItem>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
);
|
||||||
<TableRow key={headerGroup.id}>
|
})}
|
||||||
{headerGroup.headers.map((header) => {
|
</DropdownMenuContent>
|
||||||
return (
|
</DropdownMenu>
|
||||||
<TableHead key={header.id}>
|
</div>
|
||||||
{header.isPlaceholder
|
<div className="rounded-md border">
|
||||||
? null
|
{isLoading ? (
|
||||||
: flexRender(
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
header.column.columnDef.header,
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
header.getContext(),
|
Loading...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : data?.length === 0 ? (
|
||||||
|
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
No results.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table?.getRowModel()?.rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>No results.</>
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableCell>
|
||||||
);
|
</TableRow>
|
||||||
})}
|
)}
|
||||||
</TableRow>
|
</TableBody>
|
||||||
))}
|
</Table>
|
||||||
</TableHeader>
|
)}
|
||||||
<TableBody>
|
</div>
|
||||||
{table?.getRowModel()?.rows?.length ? (
|
{data && data?.length > 0 && (
|
||||||
table.getRowModel().rows.map((row) => (
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
<TableRow
|
<div className="space-x-2 flex flex-wrap">
|
||||||
key={row.id}
|
<Button
|
||||||
data-state={row.getIsSelected() && "selected"}
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
{row.getVisibleCells().map((cell) => (
|
onClick={() => table.previousPage()}
|
||||||
<TableCell key={cell.id}>
|
disabled={!table.getCanPreviousPage()}
|
||||||
{flexRender(
|
>
|
||||||
cell.column.columnDef.cell,
|
Previous
|
||||||
cell.getContext(),
|
</Button>
|
||||||
)}
|
<Button
|
||||||
</TableCell>
|
variant="outline"
|
||||||
))}
|
size="sm"
|
||||||
</TableRow>
|
onClick={() => table.nextPage()}
|
||||||
))
|
disabled={!table.getCanNextPage()}
|
||||||
) : (
|
>
|
||||||
<TableRow>
|
Next
|
||||||
<TableCell
|
</Button>
|
||||||
colSpan={columns.length}
|
</div>
|
||||||
className="h-24 text-center"
|
</div>
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
|
||||||
Loading...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>No results.</>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{data && data?.length > 0 && (
|
|
||||||
<div className="flex items-center justify-end space-x-2 py-4">
|
|
||||||
<div className="space-x-2 flex flex-wrap">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Tree } from "@/components/ui/file-tree";
|
import { Tree } from "@/components/ui/file-tree";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
import { FileIcon, Folder, Link, Loader2, Workflow } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ShowTraefikFile } from "./show-traefik-file";
|
import { ShowTraefikFile } from "./show-traefik-file";
|
||||||
|
|
||||||
@@ -27,53 +34,77 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("mt-6 md:grid gap-4")}>
|
<div className="w-full">
|
||||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto">
|
||||||
{isError && (
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
<AlertBlock type="error" className="w-full">
|
<CardHeader className="">
|
||||||
{error?.message}
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
</AlertBlock>
|
<FileIcon className="size-6 text-muted-foreground self-center" />
|
||||||
)}
|
Traefik File System
|
||||||
{isLoading && (
|
</CardTitle>
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
<CardDescription>
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
Manage all the files and directories in {"'/etc/dokploy/traefik'"}
|
||||||
Loading...
|
.
|
||||||
</span>
|
</CardDescription>
|
||||||
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
|
||||||
</div>
|
<AlertBlock type="warning">
|
||||||
)}
|
Adding invalid configuration to existing files, can break your
|
||||||
{directories?.length === 0 && (
|
Traefik instance, preventing access to your applications.
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
</AlertBlock>
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
</CardHeader>
|
||||||
No directories or files detected in {"'/etc/dokploy/traefik'"}
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
</span>
|
<div>
|
||||||
<Folder className="size-8 text-muted-foreground" />
|
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||||
</div>
|
{isError && (
|
||||||
)}
|
<AlertBlock type="error" className="w-full">
|
||||||
{directories && directories?.length > 0 && (
|
{error?.message}
|
||||||
<>
|
</AlertBlock>
|
||||||
<Tree
|
)}
|
||||||
data={directories}
|
{isLoading && (
|
||||||
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
onSelectChange={(item) => setFile(item?.id || null)}
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
folderIcon={Folder}
|
Loading...
|
||||||
itemIcon={Workflow}
|
</span>
|
||||||
/>
|
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||||
<div className="w-full">
|
</div>
|
||||||
{file ? (
|
)}
|
||||||
<ShowTraefikFile path={file} serverId={serverId} />
|
{directories?.length === 0 && (
|
||||||
) : (
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
No directories or files detected in{" "}
|
||||||
No file selected
|
{"'/etc/dokploy/traefik'"}
|
||||||
</span>
|
</span>
|
||||||
<FileIcon className="size-8 text-muted-foreground" />
|
<Folder className="size-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{directories && directories?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Tree
|
||||||
|
data={directories}
|
||||||
|
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
|
||||||
|
onSelectChange={(item) => setFile(item?.id || null)}
|
||||||
|
folderIcon={Folder}
|
||||||
|
itemIcon={Workflow}
|
||||||
|
/>
|
||||||
|
<div className="w-full">
|
||||||
|
{file ? (
|
||||||
|
<ShowTraefikFile path={file} serverId={serverId} />
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
No file selected
|
||||||
|
</span>
|
||||||
|
<FileIcon className="size-8 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</CardContent>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
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 { ShowMariadbResources } from "./show-mariadb-resources";
|
|
||||||
|
|
||||||
const addDockerImage = z.object({
|
|
||||||
dockerImage: z.string().min(1, "Docker image is required"),
|
|
||||||
command: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddDockerImage = z.infer<typeof addDockerImage>;
|
|
||||||
export const ShowAdvancedMariadb = ({ mariadbId }: Props) => {
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
|
||||||
{
|
|
||||||
mariadbId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mariadbId },
|
|
||||||
);
|
|
||||||
const { mutateAsync } = api.mariadb.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.formState.isSubmitSuccessful, form.reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: AddDockerImage) => {
|
|
||||||
await mutateAsync({
|
|
||||||
mariadbId,
|
|
||||||
dockerImage: formData?.dockerImage,
|
|
||||||
command: formData?.command,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Docker Image Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating 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="mariadb: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 mariadbId={mariadbId} />
|
|
||||||
<ShowMariadbResources mariadbId={mariadbId} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
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 {
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipContent,
|
|
||||||
Tooltip,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const addResourcesMariadb = z.object({
|
|
||||||
memoryReservation: z.number().nullable().optional(),
|
|
||||||
cpuLimit: z.number().nullable().optional(),
|
|
||||||
memoryLimit: z.number().nullable().optional(),
|
|
||||||
cpuReservation: z.number().nullable().optional(),
|
|
||||||
});
|
|
||||||
interface Props {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddResourcesMariadb = z.infer<typeof addResourcesMariadb>;
|
|
||||||
export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
|
||||||
{
|
|
||||||
mariadbId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mariadbId },
|
|
||||||
);
|
|
||||||
const { mutateAsync, isLoading } = api.mariadb.update.useMutation();
|
|
||||||
const form = useForm<AddResourcesMariadb>({
|
|
||||||
defaultValues: {},
|
|
||||||
resolver: zodResolver(addResourcesMariadb),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
cpuLimit: data?.cpuLimit || undefined,
|
|
||||||
cpuReservation: data?.cpuReservation || undefined,
|
|
||||||
memoryLimit: data?.memoryLimit || undefined,
|
|
||||||
memoryReservation: data?.memoryReservation || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data, form, form.formState.isSubmitSuccessful, form.reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: AddResourcesMariadb) => {
|
|
||||||
await mutateAsync({
|
|
||||||
mariadbId,
|
|
||||||
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 updating 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>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory soft limit in bytes. Example: 256MB =
|
|
||||||
268435456 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="268435456 (256MB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="memoryLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory hard limit in bytes. Example: 1GB =
|
|
||||||
1073741824 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1073741824 (1GB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
|
||||||
CPUs = 2000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="2000000000 (2 CPUs)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuReservation"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU shares (relative weight). Example: 1 CPU =
|
|
||||||
1000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1000000000 (1 CPU)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
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 {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
|
||||||
const { data } = api.destination.all.useQuery();
|
|
||||||
const { data: mariadb, refetch: refetchMariadb } = api.mariadb.one.useQuery(
|
|
||||||
{
|
|
||||||
mariadbId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!mariadbId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } =
|
|
||||||
api.backup.manualBackupMariadb.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 backups to your database to save the data to a different
|
|
||||||
providers.
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{mariadb && mariadb?.backups?.length > 0 && (
|
|
||||||
<AddBackup
|
|
||||||
databaseId={mariadbId}
|
|
||||||
databaseType="mariadb"
|
|
||||||
refetch={refetchMariadb}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</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 it 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>
|
|
||||||
{mariadb?.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={mariadbId}
|
|
||||||
databaseType="mariadb"
|
|
||||||
refetch={refetchMariadb}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col pt-2">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
{mariadb?.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 creating the manual backup",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Play className="size-5 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Run Manual Backup</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<UpdateBackup
|
|
||||||
backupId={backup.backupId}
|
|
||||||
refetch={refetchMariadb}
|
|
||||||
/>
|
|
||||||
<DeleteBackup
|
|
||||||
backupId={backup.backupId}
|
|
||||||
refetch={refetchMariadb}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
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 { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Copy, TrashIcon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const deleteMariadbSchema = z.object({
|
|
||||||
projectName: z.string().min(1, {
|
|
||||||
message: "Database name is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DeleteMariadb = z.infer<typeof deleteMariadbSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
export const DeleteMariadb = ({ mariadbId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
|
|
||||||
const { data } = api.mariadb.one.useQuery(
|
|
||||||
{ mariadbId },
|
|
||||||
{ enabled: !!mariadbId },
|
|
||||||
);
|
|
||||||
const { push } = useRouter();
|
|
||||||
const form = useForm<DeleteMariadb>({
|
|
||||||
defaultValues: {
|
|
||||||
projectName: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(deleteMariadbSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: DeleteMariadb) => {
|
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
|
||||||
if (formData.projectName === expectedName) {
|
|
||||||
await mutateAsync({ mariadbId })
|
|
||||||
.then((data) => {
|
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
|
||||||
toast.success("Database deleted successfully");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the database");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.setError("projectName", {
|
|
||||||
message: "Database name does not match",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
This action cannot be undone. This will permanently delete the
|
|
||||||
database. If you are sure please enter the database name to delete
|
|
||||||
this database.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-delete-mariadb"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="projectName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
To confirm, type{" "}
|
|
||||||
<Badge
|
|
||||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.name && data?.appName) {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${data.name}/${data.appName}`,
|
|
||||||
);
|
|
||||||
toast.success("Copied to clipboard. Be careful!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data?.name}/{data?.appName}
|
|
||||||
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
|
||||||
</Badge>{" "}
|
|
||||||
in the box below:
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter database name to confirm"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-delete-mariadb"
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
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 {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|
||||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveEnvironment.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
|
||||||
{
|
|
||||||
mariadbId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!mariadbId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
mariadbId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Environments Added");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error adding 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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 {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeployMariadb = ({ mariadbId }: Props) => {
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
|
||||||
{
|
|
||||||
mariadbId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mariadbId },
|
|
||||||
);
|
|
||||||
const { mutateAsync: deploy } = api.mariadb.deploy.useMutation();
|
|
||||||
const { mutateAsync: changeStatus } = api.mariadb.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 mariadb database
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await changeStatus({
|
|
||||||
mariadbId,
|
|
||||||
applicationStatus: "running",
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Deploying Database....");
|
|
||||||
await refetch();
|
|
||||||
await deploy({
|
|
||||||
mariadbId,
|
|
||||||
}).catch(() => {
|
|
||||||
toast.error("Error deploying Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error(e.message || "Error deploying Database");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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 {
|
|
||||||
mariadbId: string;
|
|
||||||
appName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ResetMariadb = ({ mariadbId, appName }: Props) => {
|
|
||||||
const { refetch } = api.mariadb.one.useQuery(
|
|
||||||
{
|
|
||||||
mariadbId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mariadbId },
|
|
||||||
);
|
|
||||||
const { mutateAsync: reload, isLoading } = api.mariadb.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({
|
|
||||||
mariadbId,
|
|
||||||
appName,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Service Reloaded");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading the service");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,26 +1,62 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
import { StartMariadb } from "../start-mariadb";
|
|
||||||
import { DeployMariadb } from "./deploy-mariadb";
|
|
||||||
import { ResetMariadb } from "./reset-mariadb";
|
|
||||||
import { StopMariadb } from "./stop-mariadb";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||||
const { data } = api.mariadb.one.useQuery(
|
const { data, refetch } = api.mariadb.one.useQuery(
|
||||||
{
|
{
|
||||||
mariadbId,
|
mariadbId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mariadbId },
|
{ enabled: !!mariadbId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
|
api.mariadb.reload.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
|
api.mariadb.start.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
|
api.mariadb.stop.useMutation();
|
||||||
|
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
api.mariadb.deployWithLogs.useSubscription(
|
||||||
|
{
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isDeploying,
|
||||||
|
onData(log) {
|
||||||
|
if (!isDrawerOpen) {
|
||||||
|
setIsDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log === "Deployment completed successfully!") {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
const parsedLogs = parseLogs(log);
|
||||||
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error("Deployment logs error:", error);
|
||||||
|
setIsDeploying(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
@@ -29,12 +65,91 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DeployMariadb mariadbId={mariadbId} />
|
<DialogAction
|
||||||
<ResetMariadb mariadbId={mariadbId} appName={data?.appName || ""} />
|
title="Deploy Mariadb"
|
||||||
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mariadb"
|
||||||
|
description="Are you sure you want to reload this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isReloading}>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<StartMariadb mariadbId={mariadbId} />
|
<DialogAction
|
||||||
|
title="Start Mariadb"
|
||||||
|
description="Are you sure you want to start this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isStarting}>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<StopMariadb mariadbId={mariadbId} />
|
<DialogAction
|
||||||
|
title="Stop Mariadb"
|
||||||
|
description="Are you sure you want to stop this mariadb?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="destructive" isLoading={isStopping}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -47,6 +162,16 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<DrawerLogs
|
||||||
|
isOpen={isDrawerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StopMariadb = ({ mariadbId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.mariadb.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({
|
|
||||||
mariadbId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.mariadb.one.invalidate({
|
|
||||||
mariadbId,
|
|
||||||
});
|
|
||||||
toast.success("Application stopped successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping the Application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StartMariadb = ({ mariadbId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.mariadb.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({
|
|
||||||
mariadbId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.mariadb.one.invalidate({
|
|
||||||
mariadbId,
|
|
||||||
});
|
|
||||||
toast.success("Database started successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting the Database");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,8 +89,12 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
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 {
|
|
||||||
mariadbId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowVolumes = ({ mariadbId }: Props) => {
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
|
||||||
{
|
|
||||||
mariadbId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mariadbId },
|
|
||||||
);
|
|
||||||
|
|
||||||
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 mariadb use the following config
|
|
||||||
to setup the volumes
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
|
||||||
<AddVolumes
|
|
||||||
serviceId={mariadbId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="mariadb"
|
|
||||||
>
|
|
||||||
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={mariadbId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="mariadb"
|
|
||||||
>
|
|
||||||
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="mariadb"
|
|
||||||
/>
|
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
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 { ShowMongoResources } from "./show-mongo-resources";
|
|
||||||
|
|
||||||
const addDockerImage = z.object({
|
|
||||||
dockerImage: z.string().min(1, "Docker image is required"),
|
|
||||||
command: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddDockerImage = z.infer<typeof addDockerImage>;
|
|
||||||
export const ShowAdvancedMongo = ({ mongoId }: Props) => {
|
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
|
||||||
{
|
|
||||||
mongoId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mongoId },
|
|
||||||
);
|
|
||||||
const { mutateAsync } = api.mongo.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.formState.isSubmitSuccessful, form.reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: AddDockerImage) => {
|
|
||||||
await mutateAsync({
|
|
||||||
mongoId,
|
|
||||||
dockerImage: formData?.dockerImage,
|
|
||||||
command: formData?.command,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Resources Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating 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="mongo: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 mongoId={mongoId} />
|
|
||||||
<ShowMongoResources mongoId={mongoId} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
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 {
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipContent,
|
|
||||||
Tooltip,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const addResourcesMongo = z.object({
|
|
||||||
memoryReservation: z.number().nullable().optional(),
|
|
||||||
cpuLimit: z.number().nullable().optional(),
|
|
||||||
memoryLimit: z.number().nullable().optional(),
|
|
||||||
cpuReservation: z.number().nullable().optional(),
|
|
||||||
});
|
|
||||||
interface Props {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddResourcesMongo = z.infer<typeof addResourcesMongo>;
|
|
||||||
export const ShowMongoResources = ({ mongoId }: Props) => {
|
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
|
||||||
{
|
|
||||||
mongoId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mongoId },
|
|
||||||
);
|
|
||||||
const { mutateAsync, isLoading } = api.mongo.update.useMutation();
|
|
||||||
const form = useForm<AddResourcesMongo>({
|
|
||||||
defaultValues: {},
|
|
||||||
resolver: zodResolver(addResourcesMongo),
|
|
||||||
});
|
|
||||||
|
|
||||||
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: AddResourcesMongo) => {
|
|
||||||
await mutateAsync({
|
|
||||||
mongoId,
|
|
||||||
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 updating 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>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory soft limit in bytes. Example: 256MB =
|
|
||||||
268435456 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="268435456 (256MB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="memoryLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory hard limit in bytes. Example: 1GB =
|
|
||||||
1073741824 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1073741824 (1GB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
|
||||||
CPUs = 2000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="2000000000 (2 CPUs)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuReservation"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU shares (relative weight). Example: 1 CPU =
|
|
||||||
1000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1000000000 (1 CPU)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
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 {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowBackupMongo = ({ mongoId }: Props) => {
|
|
||||||
const { data } = api.destination.all.useQuery();
|
|
||||||
const { data: mongo, refetch: refetchMongo } = api.mongo.one.useQuery(
|
|
||||||
{
|
|
||||||
mongoId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!mongoId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } =
|
|
||||||
api.backup.manualBackupMongo.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 backups to your database to save the data to a different
|
|
||||||
provider.
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{mongo && mongo?.backups?.length > 0 && (
|
|
||||||
<AddBackup
|
|
||||||
databaseId={mongoId}
|
|
||||||
databaseType="mongo"
|
|
||||||
refetch={refetchMongo}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</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 it 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>
|
|
||||||
{mongo?.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={mongoId}
|
|
||||||
databaseType="mongo"
|
|
||||||
refetch={refetchMongo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col pt-2">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
{mongo?.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 creating the manual backup",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Play className="size-5 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Run Manual Backup</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<UpdateBackup
|
|
||||||
backupId={backup.backupId}
|
|
||||||
refetch={refetchMongo}
|
|
||||||
/>
|
|
||||||
<DeleteBackup
|
|
||||||
backupId={backup.backupId}
|
|
||||||
refetch={refetchMongo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
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 { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Copy, TrashIcon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const deleteMongoSchema = z.object({
|
|
||||||
projectName: z.string().min(1, {
|
|
||||||
message: "Database name is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DeleteMongo = z.infer<typeof deleteMongoSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// commen
|
|
||||||
|
|
||||||
export const DeleteMongo = ({ mongoId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
|
|
||||||
const { data } = api.mongo.one.useQuery({ mongoId }, { enabled: !!mongoId });
|
|
||||||
const { push } = useRouter();
|
|
||||||
const form = useForm<DeleteMongo>({
|
|
||||||
defaultValues: {
|
|
||||||
projectName: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(deleteMongoSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: DeleteMongo) => {
|
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
|
||||||
if (formData.projectName === expectedName) {
|
|
||||||
await mutateAsync({ mongoId })
|
|
||||||
.then((data) => {
|
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
|
||||||
toast.success("Database deleted successfully");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the database");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.setError("projectName", {
|
|
||||||
message: "Database name does not match",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
This action cannot be undone. This will permanently delete the
|
|
||||||
database. If you are sure please enter the database name to delete
|
|
||||||
this database.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-delete-mongo"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="projectName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
To confirm, type{" "}
|
|
||||||
<Badge
|
|
||||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.name && data?.appName) {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${data.name}/${data.appName}`,
|
|
||||||
);
|
|
||||||
toast.success("Copied to clipboard. Be careful!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data?.name}/{data?.appName}
|
|
||||||
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
|
||||||
</Badge>{" "}
|
|
||||||
in the box below:
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter database name to confirm"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-delete-mongo"
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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 {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeployMongo = ({ mongoId }: Props) => {
|
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
|
||||||
{
|
|
||||||
mongoId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mongoId },
|
|
||||||
);
|
|
||||||
const { mutateAsync: deploy } = api.mongo.deploy.useMutation();
|
|
||||||
const { mutateAsync: changeStatus } = api.mongo.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 mongo database
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await changeStatus({
|
|
||||||
mongoId,
|
|
||||||
applicationStatus: "running",
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Deploying Database....");
|
|
||||||
await refetch();
|
|
||||||
await deploy({
|
|
||||||
mongoId,
|
|
||||||
}).catch(() => {
|
|
||||||
toast.error("Error deploying Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error(e.message || "Error deploying Database");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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 {
|
|
||||||
mongoId: string;
|
|
||||||
appName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ResetMongo = ({ mongoId, appName }: Props) => {
|
|
||||||
const { refetch } = api.mongo.one.useQuery(
|
|
||||||
{
|
|
||||||
mongoId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mongoId },
|
|
||||||
);
|
|
||||||
const { mutateAsync: reload, isLoading } = api.mongo.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({
|
|
||||||
mongoId,
|
|
||||||
appName,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Service Reloaded");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading the service");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,24 +1,61 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
import { StartMongo } from "../start-mongo";
|
|
||||||
import { DeployMongo } from "./deploy-mongo";
|
|
||||||
import { ResetMongo } from "./reset-mongo";
|
|
||||||
import { StopMongo } from "./stop-mongo";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||||
const { data } = api.mongo.one.useQuery(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mongoId },
|
{ enabled: !!mongoId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
|
api.mongo.reload.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
|
api.mongo.start.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
|
api.mongo.stop.useMutation();
|
||||||
|
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
api.mongo.deployWithLogs.useSubscription(
|
||||||
|
{
|
||||||
|
mongoId: mongoId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isDeploying,
|
||||||
|
onData(log) {
|
||||||
|
if (!isDrawerOpen) {
|
||||||
|
setIsDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log === "Deployment completed successfully!") {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedLogs = parseLogs(log);
|
||||||
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error("Deployment logs error:", error);
|
||||||
|
setIsDeploying(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
@@ -27,12 +64,92 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DeployMongo mongoId={mongoId} />
|
<DialogAction
|
||||||
<ResetMongo mongoId={mongoId} appName={data?.appName || ""} />
|
title="Deploy Mongo"
|
||||||
|
description="Are you sure you want to deploy this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mongo"
|
||||||
|
description="Are you sure you want to reload this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
mongoId: mongoId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isReloading}>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<StartMongo mongoId={mongoId} />
|
<DialogAction
|
||||||
|
title="Start Mongo"
|
||||||
|
description="Are you sure you want to start this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mongoId: mongoId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isStarting}>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<StopMongo mongoId={mongoId} />
|
<DialogAction
|
||||||
|
title="Stop Mongo"
|
||||||
|
description="Are you sure you want to stop this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mongoId: mongoId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="destructive" isLoading={isStopping}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -45,6 +162,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<DrawerLogs
|
||||||
|
isOpen={isDrawerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StopMongo = ({ mongoId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.mongo.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({
|
|
||||||
mongoId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.mongo.one.invalidate({
|
|
||||||
mongoId,
|
|
||||||
});
|
|
||||||
toast.success("Application stopped successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping the Application");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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 {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StartMongo = ({ mongoId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.mongo.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({
|
|
||||||
mongoId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.mongo.one.invalidate({
|
|
||||||
mongoId,
|
|
||||||
});
|
|
||||||
toast.success("Database started successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting the Database");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -91,8 +91,12 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
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 {
|
|
||||||
mongoId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowVolumes = ({ mongoId }: Props) => {
|
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
|
||||||
{
|
|
||||||
mongoId,
|
|
||||||
},
|
|
||||||
{ enabled: !!mongoId },
|
|
||||||
);
|
|
||||||
|
|
||||||
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 mongo use the following config.
|
|
||||||
to setup the volumes
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
|
||||||
<AddVolumes serviceId={mongoId} refetch={refetch} serviceType="mongo">
|
|
||||||
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={mongoId}
|
|
||||||
refetch={refetch}
|
|
||||||
serviceType="mongo"
|
|
||||||
>
|
|
||||||
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="mongo"
|
|
||||||
/>
|
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -187,81 +187,127 @@ export const DockerMonitoring = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className="bg-background">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||||
<CardHeader>
|
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
|
||||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
<header className="flex items-center justify-between">
|
||||||
<CardDescription>
|
<div className="space-y-1">
|
||||||
Watch the usage of your server in the current app.
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
</CardDescription>
|
Monitoring
|
||||||
</CardHeader>
|
</h1>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<p className="text-sm text-muted-foreground">
|
||||||
<div className="flex w-full gap-8 ">
|
Watch the usage of your server in the current app
|
||||||
<div className=" flex-row gap-8 grid md:grid-cols-2 w-full">
|
</p>
|
||||||
<div className="flex flex-col gap-2 w-full ">
|
</div>
|
||||||
<span className="text-base font-medium">CPU</span>
|
</header>
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Used: {currentData.cpu.value.toFixed(2)}%
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
</span>
|
<Card className="bg-background">
|
||||||
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
|
||||||
</div>
|
</CardHeader>
|
||||||
<div className="flex flex-col gap-2 w-full ">
|
<CardContent>
|
||||||
<span className="text-base font-medium">Memory</span>
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
|
|
||||||
</span>
|
|
||||||
<Progress
|
|
||||||
value={currentData.memory.value.usedPercentage}
|
|
||||||
className="w-[100%]"
|
|
||||||
/>
|
|
||||||
<DockerMemoryChart
|
|
||||||
acummulativeData={acummulativeData.memory}
|
|
||||||
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{appName === "dokploy" && (
|
|
||||||
<div className="flex flex-col gap-2 w-full ">
|
|
||||||
<span className="text-base font-medium">Space</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
|
Used: {currentData.cpu.value.toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
<Progress
|
<Progress
|
||||||
value={currentData.disk.value.diskUsedPercentage}
|
value={currentData.cpu.value}
|
||||||
className="w-[100%]"
|
className="w-[100%]"
|
||||||
/>
|
/>
|
||||||
<DockerDiskChart
|
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||||
acummulativeData={acummulativeData.disk}
|
</div>
|
||||||
diskTotal={currentData.disk.value.diskTotal}
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Memory Usage
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{`Used: ${(currentData.memory.value.used / 1024 ** 3).toFixed(2)} GB / Limit: ${(currentData.memory.value.total / 1024 ** 3).toFixed(2)} GB`}
|
||||||
|
</span>
|
||||||
|
<Progress
|
||||||
|
value={currentData.memory.value.usedPercentage}
|
||||||
|
className="w-[100%]"
|
||||||
|
/>
|
||||||
|
<DockerMemoryChart
|
||||||
|
acummulativeData={acummulativeData.memory}
|
||||||
|
memoryLimitGB={currentData.memory.value.total / 1024 ** 3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CardContent>
|
||||||
<div className="flex flex-col gap-2 w-full ">
|
</Card>
|
||||||
<span className="text-base font-medium">Block I/O</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
{appName === "dokploy" && (
|
||||||
{`Read: ${currentData.block.value.readMb.toFixed(
|
<Card className="bg-background">
|
||||||
2,
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
|
<CardTitle className="text-sm font-medium">
|
||||||
3,
|
Disk Space
|
||||||
)} MB`}
|
</CardTitle>
|
||||||
</span>
|
</CardHeader>
|
||||||
<DockerBlockChart acummulativeData={acummulativeData.block} />
|
<CardContent>
|
||||||
</div>
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<div className="flex flex-col gap-2 w-full ">
|
<span className="text-sm text-muted-foreground">
|
||||||
<span className="text-base font-medium">Network</span>
|
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
|
||||||
<span className="text-sm text-muted-foreground">
|
</span>
|
||||||
{`In MB: ${currentData.network.value.inputMb.toFixed(
|
<Progress
|
||||||
2,
|
value={currentData.disk.value.diskUsedPercentage}
|
||||||
)} MB / Out MB: ${currentData.network.value.outputMb.toFixed(
|
className="w-[100%]"
|
||||||
2,
|
/>
|
||||||
)} MB`}
|
<DockerDiskChart
|
||||||
</span>
|
acummulativeData={acummulativeData.disk}
|
||||||
<DockerNetworkChart
|
diskTotal={currentData.disk.value.diskTotal}
|
||||||
acummulativeData={acummulativeData.network}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Block I/O</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{`Read: ${currentData.block.value.readMb.toFixed(
|
||||||
|
2,
|
||||||
|
)} MB / Write: ${currentData.block.value.writeMb.toFixed(
|
||||||
|
3,
|
||||||
|
)} MB`}
|
||||||
|
</span>
|
||||||
|
<DockerBlockChart acummulativeData={acummulativeData.block} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">
|
||||||
|
Network I/O
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{`In MB: ${currentData.network.value.inputMb.toFixed(
|
||||||
|
2,
|
||||||
|
)} MB / Out MB: ${currentData.network.value.outputMb.toFixed(
|
||||||
|
2,
|
||||||
|
)} MB`}
|
||||||
|
</span>
|
||||||
|
<DockerNetworkChart
|
||||||
|
acummulativeData={acummulativeData.network}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
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 updating 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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
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 backups to your database to save the data to a different
|
|
||||||
provider.
|
|
||||||
</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 it 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 creating 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
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 { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Copy, TrashIcon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const deleteMysqlSchema = z.object({
|
|
||||||
projectName: z.string().min(1, {
|
|
||||||
message: "Database name is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DeleteMysql = z.infer<typeof deleteMysqlSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
mysqlId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteMysql = ({ mysqlId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
|
|
||||||
const { data } = api.mysql.one.useQuery({ mysqlId }, { enabled: !!mysqlId });
|
|
||||||
const { push } = useRouter();
|
|
||||||
const form = useForm<DeleteMysql>({
|
|
||||||
defaultValues: {
|
|
||||||
projectName: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(deleteMysqlSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: DeleteMysql) => {
|
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
|
||||||
if (formData.projectName === expectedName) {
|
|
||||||
await mutateAsync({ mysqlId })
|
|
||||||
.then((data) => {
|
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
|
||||||
toast.success("Database deleted successfully");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the database");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.setError("projectName", {
|
|
||||||
message: "Database name does not match",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
This action cannot be undone. This will permanently delete the
|
|
||||||
database. If you are sure please enter the database name to delete
|
|
||||||
this database.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-delete-mysql"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="projectName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
To confirm, type{" "}
|
|
||||||
<Badge
|
|
||||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.name && data?.appName) {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${data.name}/${data.appName}`,
|
|
||||||
);
|
|
||||||
toast.success("Copied to clipboard. Be careful!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data?.name}/{data?.appName}
|
|
||||||
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
|
||||||
</Badge>{" "}
|
|
||||||
in the box below:
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter database name to confirm"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-delete-mysql"
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
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 adding 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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 deploying Database");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error(e.message || "Error deploying Database");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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 reloading the service");
|
|
||||||
});
|
|
||||||
await refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,24 +1,59 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
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 {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||||
const { data } = api.mysql.one.useQuery(
|
const { data, refetch } = api.mysql.one.useQuery(
|
||||||
{
|
{
|
||||||
mysqlId,
|
mysqlId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mysqlId },
|
{ enabled: !!mysqlId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
|
api.mysql.reload.useMutation();
|
||||||
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
|
api.mysql.start.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
|
api.mysql.stop.useMutation();
|
||||||
|
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
api.mysql.deployWithLogs.useSubscription(
|
||||||
|
{
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isDeploying,
|
||||||
|
onData(log) {
|
||||||
|
if (!isDrawerOpen) {
|
||||||
|
setIsDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log === "Deployment completed successfully!") {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
const parsedLogs = parseLogs(log);
|
||||||
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error("Deployment logs error:", error);
|
||||||
|
setIsDeploying(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
@@ -27,12 +62,91 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<DeployMysql mysqlId={mysqlId} />
|
<DialogAction
|
||||||
<ResetMysql mysqlId={mysqlId} appName={data?.appName || ""} />
|
title="Deploy Mysql"
|
||||||
|
description="Are you sure you want to deploy this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mysql"
|
||||||
|
description="Are you sure you want to reload this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mysql reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Mysql");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isReloading}>
|
||||||
|
Reload
|
||||||
|
<RefreshCcw className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<StartMysql mysqlId={mysqlId} />
|
<DialogAction
|
||||||
|
title="Start Mysql"
|
||||||
|
description="Are you sure you want to start this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mysql started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mysql");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="secondary" isLoading={isStarting}>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<StopMysql mysqlId={mysqlId} />
|
<DialogAction
|
||||||
|
title="Stop Mysql"
|
||||||
|
description="Are you sure you want to stop this mysql?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mysql stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mysql");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="destructive" isLoading={isStopping}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
@@ -46,6 +160,16 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<DrawerLogs
|
||||||
|
isOpen={isDrawerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
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("MySQL stopped successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping MySQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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 successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting the Database");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -89,8 +89,12 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost">
|
<Button
|
||||||
<SquarePen className="size-4 text-muted-foreground" />
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -15,8 +15,7 @@ import React, { useEffect } from "react";
|
|||||||
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";
|
||||||
import { ShowVolumes } from "../volumes/show-volumes";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { ShowPostgresResources } from "./show-postgres-resources";
|
|
||||||
|
|
||||||
const addDockerImage = z.object({
|
const addDockerImage = z.object({
|
||||||
dockerImage: z.string().min(1, "Docker image is required"),
|
dockerImage: z.string().min(1, "Docker image is required"),
|
||||||
@@ -24,18 +23,39 @@ const addDockerImage = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
id: string;
|
||||||
|
type: Exclude<ServiceType, "application">;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddDockerImage = z.infer<typeof addDockerImage>;
|
type AddDockerImage = z.infer<typeof addDockerImage>;
|
||||||
export const ShowAdvancedPostgres = ({ postgresId }: Props) => {
|
export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||||
const { data, refetch } = api.postgres.one.useQuery(
|
const queryMap = {
|
||||||
{
|
postgres: () =>
|
||||||
postgresId,
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
},
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
{ enabled: !!postgresId },
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
);
|
mariadb: () =>
|
||||||
const { mutateAsync } = api.postgres.update.useMutation();
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
};
|
||||||
|
const { data, refetch } = queryMap[type]
|
||||||
|
? queryMap[type]()
|
||||||
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
const mutationMap = {
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = mutationMap[type]
|
||||||
|
? mutationMap[type]()
|
||||||
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<AddDockerImage>({
|
const form = useForm<AddDockerImage>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -56,16 +76,20 @@ export const ShowAdvancedPostgres = ({ postgresId }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (formData: AddDockerImage) => {
|
const onSubmit = async (formData: AddDockerImage) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
postgresId,
|
mongoId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
dockerImage: formData?.dockerImage,
|
dockerImage: formData?.dockerImage,
|
||||||
command: formData?.command,
|
command: formData?.command,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Resources Updated");
|
toast.success("Custom Command Updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the resources");
|
toast.error("Error updating the custom command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -120,8 +144,6 @@ export const ShowAdvancedPostgres = ({ postgresId }: Props) => {
|
|||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<ShowVolumes postgresId={postgresId} />
|
|
||||||
<ShowPostgresResources postgresId={postgresId} />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
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 {
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipContent,
|
|
||||||
Tooltip,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const addResourcesPostgres = z.object({
|
|
||||||
memoryReservation: z.number().nullable().optional(),
|
|
||||||
cpuLimit: z.number().nullable().optional(),
|
|
||||||
memoryLimit: z.number().nullable().optional(),
|
|
||||||
cpuReservation: z.number().nullable().optional(),
|
|
||||||
});
|
|
||||||
interface Props {
|
|
||||||
postgresId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddResourcesPostgres = z.infer<typeof addResourcesPostgres>;
|
|
||||||
export const ShowPostgresResources = ({ postgresId }: Props) => {
|
|
||||||
const { data, refetch } = api.postgres.one.useQuery(
|
|
||||||
{
|
|
||||||
postgresId,
|
|
||||||
},
|
|
||||||
{ enabled: !!postgresId },
|
|
||||||
);
|
|
||||||
const { mutateAsync, isLoading } = api.postgres.update.useMutation();
|
|
||||||
const form = useForm<AddResourcesPostgres>({
|
|
||||||
defaultValues: {},
|
|
||||||
resolver: zodResolver(addResourcesPostgres),
|
|
||||||
});
|
|
||||||
|
|
||||||
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: AddResourcesPostgres) => {
|
|
||||||
await mutateAsync({
|
|
||||||
postgresId,
|
|
||||||
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 updating 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>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory soft limit in bytes. Example: 256MB =
|
|
||||||
268435456 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="268435456 (256MB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="memoryLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Memory hard limit in bytes. Example: 1GB =
|
|
||||||
1073741824 bytes
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1073741824 (1GB in bytes)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuLimit"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Limit</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
|
||||||
CPUs = 2000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="2000000000 (2 CPUs)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
field.onChange(number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cpuReservation"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FormLabel>CPU Reservation</FormLabel>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
CPU shares (relative weight). Example: 1 CPU =
|
|
||||||
1000000000
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="1000000000 (1 CPU)"
|
|
||||||
{...field}
|
|
||||||
value={field.value?.toString() || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value === "") {
|
|
||||||
field.onChange(null);
|
|
||||||
} else {
|
|
||||||
const number = Number.parseInt(value, 10);
|
|
||||||
if (!Number.isNaN(number)) {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
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 { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Copy, TrashIcon } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const deletePostgresSchema = z.object({
|
|
||||||
projectName: z.string().min(1, {
|
|
||||||
message: "Database name is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DeletePostgres = z.infer<typeof deletePostgresSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
postgresId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DeletePostgres = ({ postgresId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
|
|
||||||
const { data } = api.postgres.one.useQuery(
|
|
||||||
{ postgresId },
|
|
||||||
{ enabled: !!postgresId },
|
|
||||||
);
|
|
||||||
const { push } = useRouter();
|
|
||||||
const form = useForm<DeletePostgres>({
|
|
||||||
defaultValues: {
|
|
||||||
projectName: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(deletePostgresSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: DeletePostgres) => {
|
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
|
||||||
if (formData.projectName === expectedName) {
|
|
||||||
await mutateAsync({ postgresId })
|
|
||||||
.then((data) => {
|
|
||||||
push(`/dashboard/project/${data?.projectId}`);
|
|
||||||
toast.success("Database deleted successfully");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting the database");
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.setError("projectName", {
|
|
||||||
message: "Database name does not match",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" isLoading={isLoading}>
|
|
||||||
<TrashIcon className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Are you absolutely sure?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
This action cannot be undone. This will permanently delete the
|
|
||||||
database. If you are sure please enter the database name to delete
|
|
||||||
this database.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-delete-postgres"
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="projectName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
To confirm, type{" "}
|
|
||||||
<Badge
|
|
||||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.name && data?.appName) {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${data.name}/${data.appName}`,
|
|
||||||
);
|
|
||||||
toast.success("Copied to clipboard. Be careful!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{data?.name}/{data?.appName}
|
|
||||||
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
|
||||||
</Badge>{" "}
|
|
||||||
in the box below:
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter database name to confirm"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-delete-postgres"
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
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 {
|
|
||||||
postgresId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
|
||||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
|
||||||
const { mutateAsync, isLoading } = api.postgres.saveEnvironment.useMutation();
|
|
||||||
|
|
||||||
const { data, refetch } = api.postgres.one.useQuery(
|
|
||||||
{
|
|
||||||
postgresId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!postgresId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
postgresId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Environments Added");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error adding 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user