mirror of
https://github.com/LukeHagar/dokploy.git
synced 2025-12-06 04:19:37 +00:00
Merge branch 'canary' into kucherenko/canary
This commit is contained in:
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
combine-manifests:
|
||||
docker:
|
||||
- image: cimg/node:18.18.0
|
||||
- image: cimg/node:20.9.0
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
@@ -99,14 +99,14 @@ workflows:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
- feat/add-sidebar
|
||||
- build-arm64:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
- canary
|
||||
- fix/nixpacks-version
|
||||
- feat/add-sidebar
|
||||
- combine-manifests:
|
||||
requires:
|
||||
- build-amd64
|
||||
@@ -116,4 +116,4 @@ workflows:
|
||||
only:
|
||||
- main
|
||||
- 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: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
node-version: 20.9.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
node-version: 20.9.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.0
|
||||
node-version: 20.9.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
|
||||
@@ -14,12 +14,10 @@ We have a few guidelines to follow when contributing to this project:
|
||||
|
||||
## 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.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
|
||||
```
|
||||
<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.
|
||||
|
||||
We use Node v20.9.0
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dokploy/dokploy.git
|
||||
cd dokploy
|
||||
@@ -73,9 +73,10 @@ Run the command that will spin up all the required services and files.
|
||||
pnpm run dokploy:setup
|
||||
```
|
||||
|
||||
Run this script
|
||||
Run this script
|
||||
|
||||
```bash
|
||||
pnpm run server:script
|
||||
pnpm run server:script
|
||||
```
|
||||
|
||||
Now run the development server.
|
||||
@@ -248,4 +249,3 @@ export function generate(schema: Schema): Template {
|
||||
## Docs & 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 PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /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
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-slim AS base
|
||||
FROM node:20-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /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
|
||||
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 PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /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
|
||||
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 PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -7,7 +7,7 @@ FROM base AS build
|
||||
COPY . /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
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,9 +4,9 @@ import "dotenv/config";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Queue } from "@nerimity/mimiqueue";
|
||||
import { createClient } from "redis";
|
||||
import { logger } from "./logger";
|
||||
import { type DeployJob, deployJobSchema } from "./schema";
|
||||
import { deploy } from "./utils";
|
||||
import { logger } from "./logger.js";
|
||||
import { type DeployJob, deployJobSchema } from "./schema.js";
|
||||
import { deploy } from "./utils.js";
|
||||
|
||||
const app = new Hono();
|
||||
const redisClient = createClient({
|
||||
|
||||
@@ -1 +1 @@
|
||||
18.18.0
|
||||
20.9.0
|
||||
98
apps/dokploy/__test__/deploy/github.test.ts
Normal file
98
apps/dokploy/__test__/deploy/github.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("GitHub Webhook Skip CI", () => {
|
||||
const mockGithubHeaders = {
|
||||
"x-github-event": "push",
|
||||
};
|
||||
|
||||
const createMockBody = (message: string) => ({
|
||||
head_commit: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
|
||||
const skipKeywords = [
|
||||
"[skip ci]",
|
||||
"[ci skip]",
|
||||
"[no ci]",
|
||||
"[skip actions]",
|
||||
"[actions skip]",
|
||||
];
|
||||
|
||||
it("should detect skip keywords in commit message", () => {
|
||||
for (const keyword of skipKeywords) {
|
||||
const message = `feat: add new feature ${keyword}`;
|
||||
const commitMessage = extractCommitMessage(
|
||||
mockGithubHeaders,
|
||||
createMockBody(message),
|
||||
);
|
||||
expect(commitMessage.includes(keyword)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should not detect skip keywords in normal commit message", () => {
|
||||
const message = "feat: add new feature";
|
||||
const commitMessage = extractCommitMessage(
|
||||
mockGithubHeaders,
|
||||
createMockBody(message),
|
||||
);
|
||||
for (const keyword of skipKeywords) {
|
||||
expect(commitMessage.includes(keyword)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle different webhook sources", () => {
|
||||
// GitHub
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-github-event": "push" },
|
||||
{ head_commit: { message: "[skip ci] test" } },
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
|
||||
// GitLab
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-gitlab-event": "push" },
|
||||
{ commits: [{ message: "[skip ci] test" }] },
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
|
||||
// Bitbucket
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-event-key": "repo:push" },
|
||||
{
|
||||
push: {
|
||||
changes: [{ new: { target: { message: "[skip ci] test" } } }],
|
||||
},
|
||||
},
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
|
||||
// Gitea
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-gitea-event": "push" },
|
||||
{ commits: [{ message: "[skip ci] test" }] },
|
||||
),
|
||||
).toBe("[skip ci] test");
|
||||
});
|
||||
|
||||
it("should handle missing commit message", () => {
|
||||
expect(extractCommitMessage(mockGithubHeaders, {})).toBe("NEW COMMIT");
|
||||
expect(extractCommitMessage({ "x-gitlab-event": "push" }, {})).toBe(
|
||||
"NEW COMMIT",
|
||||
);
|
||||
expect(
|
||||
extractCommitMessage(
|
||||
{ "x-event-key": "repo:push" },
|
||||
{ push: { changes: [] } },
|
||||
),
|
||||
).toBe("NEW COMMIT");
|
||||
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
|
||||
"NEW COMMIT",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
plugins: [tsconfigPaths()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@dokploy/server": path.resolve(
|
||||
|
||||
@@ -95,10 +95,10 @@ export const Login2FA = ({ authId }: Props) => {
|
||||
control={form.control}
|
||||
name="pin"
|
||||
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>
|
||||
<FormControl>
|
||||
<div className="flex justify-center">
|
||||
<div className="flex">
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
{...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";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -45,18 +45,29 @@ type AddPort = z.infer<typeof AddPortSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
portId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddPort = ({
|
||||
export const HandlePorts = ({
|
||||
applicationId,
|
||||
portId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.port.create.useMutation();
|
||||
const { data } = api.port.one.useQuery(
|
||||
{
|
||||
portId: portId ?? "",
|
||||
},
|
||||
{
|
||||
enabled: !!portId,
|
||||
},
|
||||
);
|
||||
const { mutateAsync, isLoading, error, isError } = portId
|
||||
? api.port.update.useMutation()
|
||||
: api.port.create.useMutation();
|
||||
|
||||
const form = useForm<AddPort>({
|
||||
defaultValues: {
|
||||
@@ -68,32 +79,46 @@ export const AddPort = ({
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
publishedPort: 0,
|
||||
targetPort: 0,
|
||||
publishedPort: data?.publishedPort ?? 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) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
...data,
|
||||
portId: portId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Port Created");
|
||||
toast.success(portId ? "Port Updated" : "Port Created");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the port");
|
||||
toast.error(
|
||||
portId ? "Error updating the port" : "Error creating the port",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<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>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
@@ -204,7 +229,7 @@ export const AddPort = ({
|
||||
form="hook-form-add-port"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
{portId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -7,23 +9,25 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Rss } from "lucide-react";
|
||||
import { Rss, Trash2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddPort } from "./add-port";
|
||||
import { DeletePort } from "./delete-port";
|
||||
import { UpdatePort } from "./update-port";
|
||||
import { toast } from "sonner";
|
||||
import { HandlePorts } from "./handle-ports";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowPorts = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deletePort, isLoading: isRemoving } =
|
||||
api.port.delete.useMutation();
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
@@ -35,7 +39,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
{data && data?.ports.length > 0 && (
|
||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
||||
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -45,7 +49,7 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No ports configured
|
||||
</span>
|
||||
<AddPort applicationId={applicationId}>Add Port</AddPort>
|
||||
<HandlePorts applicationId={applicationId}>Add Port</HandlePorts>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
@@ -78,8 +82,36 @@ export const ShowPorts = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<UpdatePort portId={port.portId} />
|
||||
<DeletePort portId={port.portId} />
|
||||
<HandlePorts
|
||||
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>
|
||||
|
||||
@@ -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 { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -77,19 +77,32 @@ const redirectPresets = [
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
redirectId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddRedirect = ({
|
||||
export const HandleRedirect = ({
|
||||
applicationId,
|
||||
redirectId,
|
||||
children = <PlusIcon className="w-4 h-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [presetSelected, setPresetSelected] = useState("");
|
||||
|
||||
const { data, refetch } = api.redirects.one.useQuery(
|
||||
{
|
||||
redirectId: redirectId || "",
|
||||
},
|
||||
{
|
||||
enabled: !!redirectId,
|
||||
},
|
||||
);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.redirects.create.useMutation();
|
||||
const { mutateAsync, isLoading, error, isError } = redirectId
|
||||
? api.redirects.update.useMutation()
|
||||
: api.redirects.create.useMutation();
|
||||
|
||||
const form = useForm<AddRedirect>({
|
||||
defaultValues: {
|
||||
@@ -102,29 +115,35 @@ export const AddRedirect = ({
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
permanent: false,
|
||||
regex: "",
|
||||
replacement: "",
|
||||
permanent: data?.permanent || false,
|
||||
regex: data?.regex || "",
|
||||
replacement: data?.replacement || "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (data: AddRedirect) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
...data,
|
||||
redirectId: redirectId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Redirect Created");
|
||||
toast.success(redirectId ? "Redirect Updated" : "Redirect Created");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
refetch();
|
||||
await utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
onDialogToggle(false);
|
||||
})
|
||||
.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 (
|
||||
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||
<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>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
@@ -243,7 +272,7 @@ export const AddRedirect = ({
|
||||
form="hook-form-add-redirect"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
{redirectId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -6,23 +8,28 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Split } from "lucide-react";
|
||||
import { Split, Trash2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddRedirect } from "./add-redirect";
|
||||
import { DeleteRedirect } from "./delete-redirect";
|
||||
import { UpdateRedirect } from "./update-redirect";
|
||||
import { toast } from "sonner";
|
||||
import { HandleRedirect } from "./handle-redirect";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteRedirect, isLoading: isRemoving } =
|
||||
api.redirects.delete.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
@@ -35,7 +42,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
{data && data?.redirects.length > 0 && (
|
||||
<AddRedirect applicationId={applicationId}>Add Redirect</AddRedirect>
|
||||
<HandleRedirect applicationId={applicationId}>
|
||||
Add Redirect
|
||||
</HandleRedirect>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -45,9 +54,9 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No redirects configured
|
||||
</span>
|
||||
<AddRedirect applicationId={applicationId}>
|
||||
<HandleRedirect applicationId={applicationId}>
|
||||
Add Redirect
|
||||
</AddRedirect>
|
||||
</HandleRedirect>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
@@ -76,8 +85,40 @@ export const ShowRedirects = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<UpdateRedirect redirectId={redirect.redirectId} />
|
||||
<DeleteRedirect redirectId={redirect.redirectId} />
|
||||
<HandleRedirect
|
||||
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>
|
||||
|
||||
@@ -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 { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -35,17 +35,29 @@ type AddSecurity = z.infer<typeof AddSecuritychema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
securityId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AddSecurity = ({
|
||||
export const HandleSecurity = ({
|
||||
applicationId,
|
||||
securityId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
}: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.security.create.useMutation();
|
||||
const { data } = api.security.one.useQuery(
|
||||
{
|
||||
securityId: securityId ?? "",
|
||||
},
|
||||
{
|
||||
enabled: !!securityId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } = securityId
|
||||
? api.security.update.useMutation()
|
||||
: api.security.create.useMutation();
|
||||
|
||||
const form = useForm<AddSecurity>({
|
||||
defaultValues: {
|
||||
@@ -56,16 +68,20 @@ export const AddSecurity = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||
form.reset({
|
||||
username: data?.username || "",
|
||||
password: data?.password || "",
|
||||
});
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (data: AddSecurity) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
...data,
|
||||
securityId: securityId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Security Created");
|
||||
toast.success(securityId ? "Security Updated" : "Security Created");
|
||||
await utils.application.one.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
@@ -75,20 +91,34 @@ export const AddSecurity = ({
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating security");
|
||||
toast.error(
|
||||
securityId
|
||||
? "Error updating the security"
|
||||
: "Error creating security",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<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>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Security</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add security to your application
|
||||
{securityId ? "Update" : "Add"} security to your application
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
@@ -137,7 +167,7 @@ export const AddSecurity = ({
|
||||
form="hook-form-add-security"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
{securityId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -6,23 +8,27 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { LockKeyhole } from "lucide-react";
|
||||
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddSecurity } from "./add-security";
|
||||
import { DeleteSecurity } from "./delete-security";
|
||||
import { UpdateSecurity } from "./update-security";
|
||||
import { toast } from "sonner";
|
||||
import { HandleSecurity } from "./handle-security";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.one.useQuery(
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteSecurity, isLoading: isRemoving } =
|
||||
api.security.delete.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
@@ -32,7 +38,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
{data && data?.security.length > 0 && (
|
||||
<AddSecurity applicationId={applicationId}>Add Security</AddSecurity>
|
||||
<HandleSecurity applicationId={applicationId}>
|
||||
Add Security
|
||||
</HandleSecurity>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -42,9 +50,9 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No security configured
|
||||
</span>
|
||||
<AddSecurity applicationId={applicationId}>
|
||||
<HandleSecurity applicationId={applicationId}>
|
||||
Add Security
|
||||
</AddSecurity>
|
||||
</HandleSecurity>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2">
|
||||
@@ -67,8 +75,39 @@ export const ShowSecurity = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<UpdateSecurity securityId={security.securityId} />
|
||||
<DeleteSecurity securityId={security.securityId} />
|
||||
<HandleSecurity
|
||||
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>
|
||||
|
||||
@@ -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,
|
||||
} 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 {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
Tooltip,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
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 addResourcesMysql = z.object({
|
||||
memoryReservation: z.number().nullable().optional(),
|
||||
cpuLimit: z.number().nullable().optional(),
|
||||
memoryLimit: z.number().nullable().optional(),
|
||||
cpuReservation: z.number().nullable().optional(),
|
||||
const addResourcesSchema = z.object({
|
||||
memoryReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ServiceType =
|
||||
| "postgres"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "application";
|
||||
|
||||
interface Props {
|
||||
mysqlId: string;
|
||||
id: string;
|
||||
type: ServiceType | "application";
|
||||
}
|
||||
|
||||
type AddResourcesMysql = z.infer<typeof addResourcesMysql>;
|
||||
export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
const { data, refetch } = api.mysql.one.useQuery(
|
||||
{
|
||||
mysqlId,
|
||||
type AddResources = z.infer<typeof addResourcesSchema>;
|
||||
export const ShowResources = ({ 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 }),
|
||||
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 },
|
||||
);
|
||||
const { mutateAsync, isLoading } = api.mysql.update.useMutation();
|
||||
const form = useForm<AddResourcesMysql>({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(addResourcesMysql),
|
||||
resolver: zodResolver(addResourcesSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,9 +101,14 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: AddResourcesMysql) => {
|
||||
const onSubmit = async (formData: AddResources) => {
|
||||
await mutateAsync({
|
||||
mysqlId,
|
||||
mongoId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
applicationId: id || "",
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
@@ -81,6 +122,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
toast.error("Error updating the resources");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
@@ -127,18 +169,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
<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 />
|
||||
@@ -172,18 +202,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
<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 />
|
||||
@@ -219,17 +237,6 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
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 />
|
||||
@@ -260,22 +267,7 @@ export const ShowMysqlResources = ({ mysqlId }: Props) => {
|
||||
</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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input placeholder="1000000000 (1 CPU)" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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 { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -7,40 +9,49 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Package } from "lucide-react";
|
||||
import { Package, Trash2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ServiceType } from "../show-resources";
|
||||
import { AddVolumes } from "./add-volumes";
|
||||
import { DeleteVolume } from "./delete-volume";
|
||||
import { UpdateVolume } from "./update-volume";
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
id: string;
|
||||
type: ServiceType | "compose";
|
||||
}
|
||||
|
||||
export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
|
||||
export const ShowVolumes = ({ 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 }),
|
||||
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 (
|
||||
<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 application use the following
|
||||
config to setup the volumes
|
||||
If you want to persist data in this postgres database use the
|
||||
following config to setup the volumes
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{data && data?.mounts.length > 0 && (
|
||||
<AddVolumes
|
||||
serviceId={applicationId}
|
||||
refetch={refetch}
|
||||
serviceType="application"
|
||||
>
|
||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
)}
|
||||
@@ -52,17 +63,13 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
<span className="text-base text-muted-foreground">
|
||||
No volumes/mounts configured
|
||||
</span>
|
||||
<AddVolumes
|
||||
serviceId={applicationId}
|
||||
refetch={refetch}
|
||||
serviceType="application"
|
||||
>
|
||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||
Add Volume
|
||||
</AddVolumes>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 gap-4">
|
||||
<AlertBlock type="info">
|
||||
<AlertBlock type="warning">
|
||||
Please remember to click Redeploy after adding, editing, or
|
||||
deleting a mount to apply the changes.
|
||||
</AlertBlock>
|
||||
@@ -73,7 +80,8 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
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">
|
||||
{/* <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">
|
||||
<span className="font-medium">Mount Type</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
@@ -90,21 +98,12 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
<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">
|
||||
@@ -126,9 +125,34 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
||||
mountId={mount.mountId}
|
||||
type={mount.type}
|
||||
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>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { PenBoxIcon, Pencil } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -177,8 +177,13 @@ export const UpdateVolume = ({
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<Pencil className="size-4 text-muted-foreground" />
|
||||
<Button
|
||||
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>
|
||||
</DialogTrigger>
|
||||
<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) => {
|
||||
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
|
||||
@@ -104,9 +104,7 @@ export const AddDomain = ({
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "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 {
|
||||
Card,
|
||||
@@ -8,17 +9,17 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 { toast } from "sonner";
|
||||
import { AddDomain } from "./add-domain";
|
||||
import { DeleteDomain } from "./delete-domain";
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowDomains = ({ applicationId }: Props) => {
|
||||
const { data } = api.domain.byApplicationId.useQuery(
|
||||
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
@@ -26,6 +27,10 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||
api.domain.delete.useMutation();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
@@ -97,7 +102,32 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -18,10 +18,11 @@ 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 React, { type CSSProperties, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { ServiceType } from "../advanced/show-resources";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
environment: z.string(),
|
||||
@@ -30,21 +31,39 @@ const addEnvironmentSchema = z.object({
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
|
||||
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 { mutateAsync, isLoading } = api.mongo.saveEnvironment.useMutation();
|
||||
|
||||
const { data, refetch } = api.mongo.one.useQuery(
|
||||
{
|
||||
mongoId,
|
||||
},
|
||||
{
|
||||
enabled: !!mongoId,
|
||||
},
|
||||
);
|
||||
const mutationMap = {
|
||||
postgres: () => api.postgres.update.useMutation(),
|
||||
redis: () => api.redis.update.useMutation(),
|
||||
mysql: () => api.mysql.update.useMutation(),
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
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>({
|
||||
defaultValues: {
|
||||
environment: "",
|
||||
@@ -62,8 +81,13 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||
|
||||
const onSubmit = async (data: EnvironmentSchema) => {
|
||||
mutateAsync({
|
||||
mongoId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
composeId: id || "",
|
||||
env: data.environment,
|
||||
mongoId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Environments Added");
|
||||
@@ -111,6 +135,11 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
style={
|
||||
{
|
||||
WebkitTextSecurity: isEnvVisible ? "disc" : null,
|
||||
} as CSSProperties
|
||||
}
|
||||
language="properties"
|
||||
disabled={isEnvVisible}
|
||||
placeholder={`NODE_ENV=production
|
||||
@@ -55,7 +55,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
return (
|
||||
<Card className="bg-background px-6 pb-6">
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -97,5 +97,5 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
</form>
|
||||
</Form>
|
||||
</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 { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
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 { toast } from "sonner";
|
||||
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 {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
@@ -25,6 +23,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
{ enabled: !!applicationId },
|
||||
);
|
||||
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 (
|
||||
<>
|
||||
@@ -33,17 +43,127 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<DeployApplication applicationId={applicationId} />
|
||||
<ResetApplication
|
||||
applicationId={applicationId}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
<DialogAction
|
||||
title="Deploy Application"
|
||||
description="Are you sure you want to deploy this application?"
|
||||
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" ? (
|
||||
<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
|
||||
appName={data?.appName || ""}
|
||||
|
||||
@@ -104,9 +104,7 @@ export const AddPreviewDomain = ({
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "In this section you can edit a domain"
|
||||
|
||||
@@ -291,16 +291,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
"PORT=3000",
|
||||
].join("\n")}
|
||||
/>
|
||||
{/* <CodeEditor
|
||||
lineWrapping
|
||||
language="properties"
|
||||
wrapperClassName="h-[25rem] font-mono"
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
`}
|
||||
{...field}
|
||||
/> */}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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 { api } from "@/utils/api";
|
||||
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 { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -91,8 +91,12 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
<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>
|
||||
</DialogTrigger>
|
||||
<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 {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Copy } from "lucide-react";
|
||||
import { Copy, Trash2 } from "lucide-react";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
@@ -82,8 +81,13 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
<Button
|
||||
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>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -20,6 +20,11 @@ interface Props {
|
||||
|
||||
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
|
||||
@@ -126,9 +126,7 @@ export const AddDomainCompose = ({
|
||||
|
||||
const dictionary = {
|
||||
success: domainId ? "Domain Updated" : "Domain Created",
|
||||
error: domainId
|
||||
? "Error updating the domain"
|
||||
: "Error creating the domain",
|
||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||
submit: domainId ? "Update" : "Create",
|
||||
dialogDescription: domainId
|
||||
? "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 {
|
||||
Card,
|
||||
@@ -8,9 +9,9 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 { DeleteDomain } from "../../application/domains/delete-domain";
|
||||
import { toast } from "sonner";
|
||||
import { AddDomainCompose } from "./add-domain";
|
||||
|
||||
interface Props {
|
||||
@@ -18,7 +19,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
const { data } = api.domain.byComposeId.useQuery(
|
||||
const { data, refetch } = api.domain.byComposeId.useQuery(
|
||||
{
|
||||
composeId,
|
||||
},
|
||||
@@ -27,6 +28,9 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||
api.domain.delete.useMutation();
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
@@ -97,7 +101,32 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Ban, CheckCircle2, Hammer, Terminal } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "sonner";
|
||||
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 {
|
||||
composeId: string;
|
||||
}
|
||||
export const ComposeActions = ({ composeId }: Props) => {
|
||||
const router = useRouter();
|
||||
const { data, refetch } = api.compose.one.useQuery(
|
||||
{
|
||||
composeId,
|
||||
@@ -30,33 +19,109 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
{ enabled: !!composeId },
|
||||
);
|
||||
const { mutateAsync: update } = api.compose.update.useMutation();
|
||||
|
||||
const extractDomains = (env: string) => {
|
||||
const lines = env.split("\n");
|
||||
const hostLines = lines.filter((line) => {
|
||||
const [key, value] = line.split("=");
|
||||
return key?.trim().endsWith("_HOST");
|
||||
});
|
||||
|
||||
const hosts = hostLines.map((line) => {
|
||||
const [key, value] = line.split("=");
|
||||
return value ? value.trim() : "";
|
||||
});
|
||||
|
||||
return hosts;
|
||||
};
|
||||
|
||||
const domains = extractDomains(data?.env || "");
|
||||
|
||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||
const { mutateAsync: redeploy } = api.compose.redeploy.useMutation();
|
||||
const { mutateAsync: start, isLoading: isStarting } =
|
||||
api.compose.start.useMutation();
|
||||
const { mutateAsync: stop, isLoading: isStopping } =
|
||||
api.compose.stop.useMutation();
|
||||
return (
|
||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||
<DeployCompose composeId={composeId} />
|
||||
<RedbuildCompose composeId={composeId} />
|
||||
<DialogAction
|
||||
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?.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
|
||||
@@ -89,41 +154,6 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
className="flex flex-row gap-2 items-center"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SquarePen } from "lucide-react";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -91,8 +91,12 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
<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>
|
||||
</DialogTrigger>
|
||||
<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 {
|
||||
Card,
|
||||
@@ -13,31 +14,47 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { DatabaseBackup, Play } from "lucide-react";
|
||||
import { DatabaseBackup, Play, Trash2 } 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";
|
||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||
import { AddBackup } from "./add-backup";
|
||||
import { UpdateBackup } from "./update-backup";
|
||||
|
||||
interface Props {
|
||||
postgresId: string;
|
||||
id: string;
|
||||
type: Exclude<ServiceType, "application" | "redis">;
|
||||
}
|
||||
|
||||
export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
||||
export const ShowBackups = ({ id, type }: 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: postgres, refetch: refetchPostgres } =
|
||||
api.postgres.one.useQuery(
|
||||
{
|
||||
postgresId,
|
||||
},
|
||||
{
|
||||
enabled: !!postgresId,
|
||||
},
|
||||
);
|
||||
const { data: postgres, refetch } = queryMap[type]
|
||||
? queryMap[type]()
|
||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||
|
||||
const { mutateAsync: manualBackup, isLoading: isManualBackup } =
|
||||
api.backup.manualBackupPostgres.useMutation();
|
||||
const mutationMap = {
|
||||
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 (
|
||||
<Card className="bg-background">
|
||||
@@ -51,11 +68,7 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
|
||||
{postgres && postgres?.backups?.length > 0 && (
|
||||
<AddBackup
|
||||
databaseId={postgresId}
|
||||
databaseType="postgres"
|
||||
refetch={refetchPostgres}
|
||||
/>
|
||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
@@ -66,10 +79,10 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
||||
To create a backup it is required to set at least 1 provider.
|
||||
Please, go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
href="/dashboard/settings/destinations"
|
||||
className="text-foreground"
|
||||
>
|
||||
Settings
|
||||
S3 Destinations
|
||||
</Link>{" "}
|
||||
to do so.
|
||||
</span>
|
||||
@@ -83,9 +96,9 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
||||
No backups configured
|
||||
</span>
|
||||
<AddBackup
|
||||
databaseId={postgresId}
|
||||
databaseType="postgres"
|
||||
refetch={refetchPostgres}
|
||||
databaseId={id}
|
||||
databaseType={type}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -158,12 +171,34 @@ export const ShowBackupPostgres = ({ postgresId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<UpdateBackup
|
||||
backupId={backup.backupId}
|
||||
refetch={refetchPostgres}
|
||||
/>
|
||||
<DeleteBackup
|
||||
backupId={backup.backupId}
|
||||
refetch={refetchPostgres}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<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>
|
||||
@@ -116,8 +116,12 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
<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>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
|
||||
@@ -226,7 +226,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-lg overflow-hidden">
|
||||
<div className="rounded-lg">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
|
||||
@@ -9,10 +9,18 @@ import {
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { ChevronDown, Container } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -71,139 +79,164 @@ export const ShowContainers = ({ serverId }: Props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-6 grid gap-4 pb-20 w-full">
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||
<Input
|
||||
placeholder="Filter by name..."
|
||||
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) =>
|
||||
table.getColumn("name")?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="md:max-w-sm"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="sm:ml-auto max-sm:w-full">
|
||||
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{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>
|
||||
) : 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(),
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<Container className="size-6 text-muted-foreground self-center" />
|
||||
Docker Containers
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
See all the containers of your dokploy server
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
<div className="gap-4 pb-20 w-full">
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||
<Input
|
||||
placeholder="Filter by name..."
|
||||
value={
|
||||
(table.getColumn("name")?.getFilterValue() as string) ??
|
||||
""
|
||||
}
|
||||
onChange={(event) =>
|
||||
table
|
||||
.getColumn("name")
|
||||
?.setFilterValue(event.target.value)
|
||||
}
|
||||
className="md:max-w-sm"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="sm:ml-auto max-sm:w-full"
|
||||
>
|
||||
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{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>
|
||||
) : 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>
|
||||
);
|
||||
})}
|
||||
</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.</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</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>
|
||||
)}
|
||||
</TableBody>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
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 { cn } from "@/lib/utils";
|
||||
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 { ShowTraefikFile } from "./show-traefik-file";
|
||||
|
||||
@@ -27,53 +34,77 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("mt-6 md:grid gap-4")}>
|
||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="w-full">
|
||||
{error?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
{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>
|
||||
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No directories or files detected in {"'/etc/dokploy/traefik'"}
|
||||
</span>
|
||||
<Folder className="size-8 text-muted-foreground" />
|
||||
</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 className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<FileIcon className="size-6 text-muted-foreground self-center" />
|
||||
Traefik File System
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage all the files and directories in {"'/etc/dokploy/traefik'"}
|
||||
.
|
||||
</CardDescription>
|
||||
|
||||
<AlertBlock type="warning">
|
||||
Adding invalid configuration to existing files, can break your
|
||||
Traefik instance, preventing access to your applications.
|
||||
</AlertBlock>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
<div>
|
||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="w-full">
|
||||
{error?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
{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>
|
||||
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No directories or files detected in{" "}
|
||||
{"'/etc/dokploy/traefik'"}
|
||||
</span>
|
||||
<Folder className="size-8 text-muted-foreground" />
|
||||
</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>
|
||||
</Card>
|
||||
</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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Terminal } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-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 { StartMariadb } from "../start-mariadb";
|
||||
import { DeployMariadb } from "./deploy-mariadb";
|
||||
import { ResetMariadb } from "./reset-mariadb";
|
||||
import { StopMariadb } from "./stop-mariadb";
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
}
|
||||
|
||||
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
const { data } = api.mariadb.one.useQuery(
|
||||
const { data, refetch } = api.mariadb.one.useQuery(
|
||||
{
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<DeployMariadb mariadbId={mariadbId} />
|
||||
<ResetMariadb mariadbId={mariadbId} appName={data?.appName || ""} />
|
||||
<DialogAction
|
||||
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" ? (
|
||||
<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
|
||||
appName={data?.appName || ""}
|
||||
@@ -47,6 +162,16 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DrawerLogs
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsDrawerOpen(false);
|
||||
setFilteredLogs([]);
|
||||
setIsDeploying(false);
|
||||
refetch();
|
||||
}}
|
||||
filteredLogs={filteredLogs}
|
||||
/>
|
||||
</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 { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -89,8 +89,12 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
<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>
|
||||
</DialogTrigger>
|
||||
<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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Terminal } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-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 { StartMongo } from "../start-mongo";
|
||||
import { DeployMongo } from "./deploy-mongo";
|
||||
import { ResetMongo } from "./reset-mongo";
|
||||
import { StopMongo } from "./stop-mongo";
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
}
|
||||
|
||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
const { data } = api.mongo.one.useQuery(
|
||||
const { data, refetch } = api.mongo.one.useQuery(
|
||||
{
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<DeployMongo mongoId={mongoId} />
|
||||
<ResetMongo mongoId={mongoId} appName={data?.appName || ""} />
|
||||
<DialogAction
|
||||
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" ? (
|
||||
<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
|
||||
appName={data?.appName || ""}
|
||||
@@ -45,6 +162,16 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DrawerLogs
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsDrawerOpen(false);
|
||||
setFilteredLogs([]);
|
||||
setIsDeploying(false);
|
||||
refetch();
|
||||
}}
|
||||
filteredLogs={filteredLogs}
|
||||
/>
|
||||
</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 { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AlertTriangle, SquarePen } from "lucide-react";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -91,8 +91,12 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SquarePen className="size-4 text-muted-foreground" />
|
||||
<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>
|
||||
</DialogTrigger>
|
||||
<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 (
|
||||
<div>
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Monitoring</CardTitle>
|
||||
<CardDescription>
|
||||
Watch the usage of your server in the current app.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex w-full gap-8 ">
|
||||
<div className=" flex-row gap-8 grid md:grid-cols-2 w-full">
|
||||
<div className="flex flex-col gap-2 w-full ">
|
||||
<span className="text-base font-medium">CPU</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Used: {currentData.cpu.value.toFixed(2)}%
|
||||
</span>
|
||||
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full ">
|
||||
<span className="text-base font-medium">Memory</span>
|
||||
<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>
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
|
||||
<header className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Monitoring
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Watch the usage of your server in the current app
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<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>
|
||||
<Progress
|
||||
value={currentData.disk.value.diskUsedPercentage}
|
||||
value={currentData.cpu.value}
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerDiskChart
|
||||
acummulativeData={acummulativeData.disk}
|
||||
diskTotal={currentData.disk.value.diskTotal}
|
||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||
</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">
|
||||
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 className="flex flex-col gap-2 w-full ">
|
||||
<span className="text-base font-medium">Block I/O</span>
|
||||
<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>
|
||||
<div className="flex flex-col gap-2 w-full ">
|
||||
<span className="text-base font-medium">Network</span>
|
||||
<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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{appName === "dokploy" && (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Disk Space
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
|
||||
</span>
|
||||
<Progress
|
||||
value={currentData.disk.value.diskUsedPercentage}
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerDiskChart
|
||||
acummulativeData={acummulativeData.disk}
|
||||
diskTotal={currentData.disk.value.diskTotal}
|
||||
/>
|
||||
</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">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>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DockerMonitoring } from "../docker/show";
|
||||
|
||||
export const ShowMonitoring = () => {
|
||||
return (
|
||||
<div className="my-6 w-full ">
|
||||
<div className="w-full">
|
||||
<DockerMonitoring appName="dokploy" />
|
||||
</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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Terminal } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-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 { StartMysql } from "../start-mysql";
|
||||
import { DeployMysql } from "./deploy-mysql";
|
||||
import { ResetMysql } from "./reset-mysql";
|
||||
import { StopMysql } from "./stop-mysql";
|
||||
interface Props {
|
||||
mysqlId: string;
|
||||
}
|
||||
|
||||
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
const { data } = api.mysql.one.useQuery(
|
||||
const { data, refetch } = api.mysql.one.useQuery(
|
||||
{
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<DeployMysql mysqlId={mysqlId} />
|
||||
<ResetMysql mysqlId={mysqlId} appName={data?.appName || ""} />
|
||||
<DialogAction
|
||||
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" ? (
|
||||
<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
|
||||
@@ -46,6 +160,16 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DrawerLogs
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsDrawerOpen(false);
|
||||
setFilteredLogs([]);
|
||||
setIsDeploying(false);
|
||||
refetch();
|
||||
}}
|
||||
filteredLogs={filteredLogs}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user