mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-06 06:25:28 +02:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6a288781f | ||
|
|
724bed9832 | ||
|
|
2405e5a93a | ||
|
|
e97c8f42b3 | ||
|
|
d805f6a7aa | ||
|
|
45d05b2aa4 | ||
|
|
6d350a23a9 | ||
|
|
5965b73342 | ||
|
|
b8e06feaff | ||
|
|
3c5a005165 | ||
|
|
12d31c89f3 | ||
|
|
3cf7c697b8 | ||
|
|
75fc030984 | ||
|
|
060a170aee | ||
|
|
40718293a1 | ||
|
|
9ac68985e0 | ||
|
|
35ff8dcfe6 | ||
|
|
60c03e1ca7 | ||
|
|
d42fa738ea | ||
|
|
160742c2cf | ||
|
|
4c5bc541d6 | ||
|
|
d13871cd08 | ||
|
|
a12beb6748 | ||
|
|
4c90f4754f | ||
|
|
69fdda505d | ||
|
|
16e84e431a | ||
|
|
5d4db4d0f3 | ||
|
|
10d2493bcc | ||
|
|
ce97bc6c27 | ||
|
|
c2e05e86d9 | ||
|
|
5cd743eb10 | ||
|
|
cd32c55031 | ||
|
|
7f2ebab66c | ||
|
|
0bc2734925 | ||
|
|
f74d02381f | ||
|
|
d46afbef2d | ||
|
|
be64a1554d | ||
|
|
8d9d00d0c6 | ||
|
|
31164c9798 | ||
|
|
4d4de1424e | ||
|
|
fa954c3bbd | ||
|
|
005f73d665 | ||
|
|
bbe7d5bdc5 | ||
|
|
6f7a5609a3 | ||
|
|
c3a5e2a8d6 | ||
|
|
1ca965268e | ||
|
|
e323ade29e | ||
|
|
8c916bc431 | ||
|
|
0670f9b910 | ||
|
|
44f002d8d0 | ||
|
|
27f6c945e0 | ||
|
|
e61c216ea0 | ||
|
|
9f9492af79 | ||
|
|
68f608bdc9 | ||
|
|
8f671d1691 | ||
|
|
7afbe8b208 | ||
|
|
8c05214e78 | ||
|
|
07769e69d6 | ||
|
|
2ace36f035 | ||
|
|
b7196a3494 | ||
|
|
3b737ca55b | ||
|
|
581e590f65 | ||
|
|
d66a5d55a3 | ||
|
|
6df0878ed4 | ||
|
|
a1bbfaebf4 | ||
|
|
ed89f5aa8a | ||
|
|
888e904d75 | ||
|
|
3e522b9cae | ||
|
|
7903ddba89 | ||
|
|
3a0dbc26d1 | ||
|
|
6df680e9da | ||
|
|
2bced3e9b6 | ||
|
|
911a7730f9 | ||
|
|
2902648188 |
@@ -165,86 +165,8 @@ Thank you for your contribution!
|
|||||||
|
|
||||||
## Templates
|
## Templates
|
||||||
|
|
||||||
To add a new template, go to `templates` folder and create a new folder with the name of the template.
|
To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
|
||||||
|
|
||||||
Let's take the example of `plausible` template.
|
|
||||||
|
|
||||||
1. create a folder in `templates/plausible`
|
|
||||||
2. create a `docker-compose.yml` file inside the folder with the content of compose.
|
|
||||||
3. create a `index.ts` file inside the folder with the following code as base:
|
|
||||||
4. When creating a pull request, please provide a video of the template working in action.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// EXAMPLE
|
|
||||||
import {
|
|
||||||
generateBase64,
|
|
||||||
generateHash,
|
|
||||||
generateRandomDomain,
|
|
||||||
type Template,
|
|
||||||
type Schema,
|
|
||||||
type DomainSchema,
|
|
||||||
} from "../utils";
|
|
||||||
|
|
||||||
export function generate(schema: Schema): Template {
|
|
||||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
|
||||||
const mainServiceHash = generateHash(schema.projectName);
|
|
||||||
const mainDomain = generateRandomDomain(schema);
|
|
||||||
const secretBase = generateBase64(64);
|
|
||||||
const toptKeyBase = generateBase64(32);
|
|
||||||
|
|
||||||
const domains: DomainSchema[] = [
|
|
||||||
{
|
|
||||||
host: mainDomain,
|
|
||||||
port: 8000,
|
|
||||||
serviceName: "plausible",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const envs = [
|
|
||||||
`BASE_URL=http://${mainDomain}`,
|
|
||||||
`SECRET_KEY_BASE=${secretBase}`,
|
|
||||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
|
||||||
`HASH=${mainServiceHash}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
const mounts: Template["mounts"] = [
|
|
||||||
{
|
|
||||||
filePath: "./clickhouse/clickhouse-config.xml",
|
|
||||||
content: "some content......",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
envs,
|
|
||||||
mounts,
|
|
||||||
domains,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties:
|
|
||||||
|
|
||||||
**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
id: "plausible",
|
|
||||||
name: "Plausible",
|
|
||||||
version: "v2.1.0",
|
|
||||||
description:
|
|
||||||
"Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.",
|
|
||||||
logo: "plausible.svg", // we defined the name and the extension of the logo
|
|
||||||
links: {
|
|
||||||
github: "https://github.com/plausible/plausible",
|
|
||||||
website: "https://plausible.io/",
|
|
||||||
docs: "https://plausible.io/docs",
|
|
||||||
},
|
|
||||||
tags: ["analytics"],
|
|
||||||
load: () => import("./plausible/index").then((m) => m.generate),
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
|
||||||
|
|
||||||
### Recommendations
|
### Recommendations
|
||||||
|
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute.
|
|
||||||
|
|
||||||
|
|
||||||
Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues.
|
|
||||||
|
|
||||||
We have a few guidelines to follow when contributing to this project:
|
|
||||||
|
|
||||||
- [Commit Convention](#commit-convention)
|
|
||||||
- [Setup](#setup)
|
|
||||||
- [Development](#development)
|
|
||||||
- [Build](#build)
|
|
||||||
- [Pull Request](#pull-request)
|
|
||||||
|
|
||||||
## Commit Convention
|
|
||||||
|
|
||||||
Before you craete 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>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Type
|
|
||||||
Must be one of the following:
|
|
||||||
|
|
||||||
* **feat**: A new feature
|
|
||||||
* **fix**: A bug fix
|
|
||||||
* **docs**: Documentation only changes
|
|
||||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
|
||||||
* **refactor**: A code change that neither fixes a bug nor adds a feature
|
|
||||||
* **perf**: A code change that improves performance
|
|
||||||
* **test**: Adding missing tests or correcting existing tests
|
|
||||||
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
|
||||||
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
|
|
||||||
* **chore**: Other changes that don't modify `src` or `test` files
|
|
||||||
* **revert**: Reverts a previous commit
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
|
||||||
feat: add new feature
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dokploy/dokploy.git
|
|
||||||
cd dokploy
|
|
||||||
pnpm install
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Is required to have **Docker** installed on your machine.
|
|
||||||
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
Run the command that will spin up all the required services and files.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run setup
|
|
||||||
```
|
|
||||||
|
|
||||||
Now run the development server.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Go to http://localhost:3000 to see the development server
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
To build the docker image
|
|
||||||
```bash
|
|
||||||
pnpm run docker:build
|
|
||||||
```
|
|
||||||
|
|
||||||
To push the docker image
|
|
||||||
```bash
|
|
||||||
pnpm run docker:push
|
|
||||||
```
|
|
||||||
|
|
||||||
## Password Reset
|
|
||||||
|
|
||||||
In the case you lost your password, you can reset it using the following command
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run reset-password
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bunx lt --port 3000
|
|
||||||
```
|
|
||||||
|
|
||||||
If you run into permission issues of docker run the following command
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo chown -R USERNAME dokploy or sudo chown -R $(whoami) ~/.docker
|
|
||||||
```
|
|
||||||
|
|
||||||
## Application deploy
|
|
||||||
|
|
||||||
In case you want to deploy the application on your machine and you selected nixpacks or buildpacks, you need to install first.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install Nixpacks
|
|
||||||
curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|
||||||
&& chmod +x install.sh \
|
|
||||||
&& ./install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install Buildpacks
|
|
||||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Pull Request
|
|
||||||
|
|
||||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
|
||||||
- Create a new branch for each feature or bug fix.
|
|
||||||
- Make sure to add tests for your changes.
|
|
||||||
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
|
|
||||||
- When creating a pull request, please provide a clear and concise description of the changes made.
|
|
||||||
- If you include a video or screenshot, would be awesome so we can see the changes in action.
|
|
||||||
- If your pull request fixes an open issue, please reference the issue in the pull request description.
|
|
||||||
- Once your pull request is merged, you will be automatically added as a contributor to the project.
|
|
||||||
|
|
||||||
Thank you for your contribution!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Templates
|
|
||||||
|
|
||||||
To add a new template, go to `templates` folder and create a new folder with the name of the template.
|
|
||||||
|
|
||||||
Let's take the example of `plausible` template.
|
|
||||||
|
|
||||||
1. create a folder in `templates/plausible`
|
|
||||||
2. create a `docker-compose.yml` file inside the folder with the content of compose.
|
|
||||||
3. create a `index.ts` file inside the folder with the following code as base:
|
|
||||||
4. When creating a pull request, please provide a video of the template working in action.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// EXAMPLE
|
|
||||||
import {
|
|
||||||
generateHash,
|
|
||||||
generateRandomDomain,
|
|
||||||
type Template,
|
|
||||||
type Schema,
|
|
||||||
} from "../utils";
|
|
||||||
|
|
||||||
|
|
||||||
export function generate(schema: Schema): Template {
|
|
||||||
|
|
||||||
// do your stuff here, like create a new domain, generate random passwords, mounts.
|
|
||||||
const mainServiceHash = generateHash(schema.projectName);
|
|
||||||
const randomDomain = generateRandomDomain(schema);
|
|
||||||
const secretBase = generateBase64(64);
|
|
||||||
const toptKeyBase = generateBase64(32);
|
|
||||||
|
|
||||||
const envs = [
|
|
||||||
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
|
||||||
`PLAUSIBLE_HOST=${randomDomain}`,
|
|
||||||
"PLAUSIBLE_PORT=8000",
|
|
||||||
`BASE_URL=http://${randomDomain}`,
|
|
||||||
`SECRET_KEY_BASE=${secretBase}`,
|
|
||||||
`TOTP_VAULT_KEY=${toptKeyBase}`,
|
|
||||||
`HASH=${mainServiceHash}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
const mounts: Template["mounts"] = [
|
|
||||||
{
|
|
||||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
|
||||||
content: `some content......`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
envs,
|
|
||||||
mounts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties:
|
|
||||||
|
|
||||||
**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
id: "plausible",
|
|
||||||
name: "Plausible",
|
|
||||||
version: "v2.1.0",
|
|
||||||
description:
|
|
||||||
"Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.",
|
|
||||||
logo: "plausible.svg", // we defined the name and the extension of the logo
|
|
||||||
links: {
|
|
||||||
github: "https://github.com/plausible/plausible",
|
|
||||||
website: "https://plausible.io/",
|
|
||||||
docs: "https://plausible.io/docs",
|
|
||||||
},
|
|
||||||
tags: ["analytics"],
|
|
||||||
load: () => import("./plausible/index").then((m) => m.generate),
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Add the logo or image of the template to `public/templates/plausible.svg`
|
|
||||||
|
|
||||||
|
|
||||||
### Recomendations
|
|
||||||
- Use the same name of the folder as the id of the template.
|
|
||||||
- The logo should be in the public folder.
|
|
||||||
- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
|
|
||||||
- Test first on a vps or a server to make sure the template works.
|
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ if (typeof window === "undefined") {
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
|
cleanCache: false,
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { expect, test } from "vitest";
|
|||||||
const baseApp: ApplicationNested = {
|
const baseApp: ApplicationNested = {
|
||||||
applicationId: "",
|
applicationId: "",
|
||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
|
cleanCache: false,
|
||||||
applicationStatus: "done",
|
applicationStatus: "done",
|
||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
|
|||||||
@@ -468,16 +468,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* create github link */}
|
|
||||||
<div className="flex w-full justify-end">
|
|
||||||
<Link
|
|
||||||
href={`https://github.com/${repository?.owner}/${repository?.repo}`}
|
|
||||||
target="_blank"
|
|
||||||
className="w-fit"
|
|
||||||
>
|
|
||||||
Repository
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import {
|
import {
|
||||||
Ban,
|
Ban,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Hammer,
|
Hammer,
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
|
Rocket,
|
||||||
Terminal,
|
Terminal,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -55,7 +55,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Application"
|
title="Deploy Application"
|
||||||
description="Are you sure you want to deploy this application?"
|
description="Are you sure you want to deploy this application?"
|
||||||
@@ -76,25 +76,25 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Downloads the source code and performs a complete build
|
<p>
|
||||||
</p>
|
Downloads the source code and performs a complete build
|
||||||
</TooltipContent>
|
</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Application"
|
title="Reload Application"
|
||||||
@@ -114,10 +114,23 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" isLoading={isReloading}>
|
<Tooltip>
|
||||||
Reload
|
<TooltipTrigger asChild>
|
||||||
<RefreshCcw className="size-4" />
|
<Button
|
||||||
</Button>
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Reload the application without rebuilding it</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Rebuild Application"
|
title="Rebuild Application"
|
||||||
@@ -136,27 +149,25 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Rebuild
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Hammer className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Hammer className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Rebuild
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Only rebuilds the application without downloading new
|
Only rebuilds the application without downloading new code
|
||||||
code
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
@@ -177,27 +188,26 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStarting}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isStarting}
|
||||||
Start
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<CheckCircle2 className="size-4" />
|
>
|
||||||
<Tooltip>
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Start
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the application (requires a previous successful
|
Start the application (requires a previous successful
|
||||||
build)
|
build)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -216,24 +226,23 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="destructive"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStopping}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="destructive"
|
||||||
>
|
isLoading={isStopping}
|
||||||
Stop
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Ban className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Ban className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Stop
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running application</p>
|
<p>Stop the currently running application</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -241,15 +250,18 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle italic"
|
aria-label="Toggle autodeploy"
|
||||||
checked={data?.autoDeploy || false}
|
checked={data?.autoDeploy || false}
|
||||||
onCheckedChange={async (enabled) => {
|
onCheckedChange={async (enabled) => {
|
||||||
await update({
|
await update({
|
||||||
@@ -264,7 +276,29 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
toast.error("Error updating Auto Deploy");
|
toast.error("Error updating Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
|
<span className="text-sm font-medium">Clean Cache</span>
|
||||||
|
<Switch
|
||||||
|
aria-label="Toggle clean cache"
|
||||||
|
checked={data?.cleanCache || false}
|
||||||
|
onCheckedChange={async (enabled) => {
|
||||||
|
await update({
|
||||||
|
applicationId,
|
||||||
|
cleanCache: enabled,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Clean Cache Updated");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating Clean Cache");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, Hammer, HelpCircle, Terminal } from "lucide-react";
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
@@ -34,7 +34,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
api.compose.stop.useMutation();
|
api.compose.stop.useMutation();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Compose"
|
title="Deploy Compose"
|
||||||
description="Are you sure you want to deploy this compose?"
|
description="Are you sure you want to deploy this compose?"
|
||||||
@@ -55,59 +55,58 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.composeStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.composeStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads the source code and performs a complete build</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads the source code and performs a complete build</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Rebuild Compose"
|
title="Reload Compose"
|
||||||
description="Are you sure you want to rebuild this compose?"
|
description="Are you sure you want to reload this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await redeploy({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose rebuilt successfully");
|
toast.success("Compose reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error rebuilding compose");
|
toast.error("Error reloading compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.composeStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={data?.composeStatus === "running"}
|
||||||
Rebuild
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Hammer className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Only rebuilds the compose without downloading new code</p>
|
<p>Reload the compose without rebuilding it</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{data?.composeType === "docker-compose" &&
|
{data?.composeType === "docker-compose" &&
|
||||||
data?.composeStatus === "idle" ? (
|
data?.composeStatus === "idle" ? (
|
||||||
@@ -128,26 +127,25 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStarting}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isStarting}
|
||||||
Start
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<CheckCircle2 className="size-4" />
|
>
|
||||||
<Tooltip>
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Start
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the compose (requires a previous successful build)
|
Start the compose (requires a previous successful build)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -166,24 +164,23 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="destructive"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStopping}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="destructive"
|
||||||
>
|
isLoading={isStopping}
|
||||||
Stop
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Ban className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Ban className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Stop
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running compose</p>
|
<p>Stop the currently running compose</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -191,15 +188,18 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle italic"
|
aria-label="Toggle autodeploy"
|
||||||
checked={data?.autoDeploy || false}
|
checked={data?.autoDeploy || false}
|
||||||
onCheckedChange={async (enabled) => {
|
onCheckedChange={async (enabled) => {
|
||||||
await update({
|
await update({
|
||||||
@@ -214,7 +214,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
toast.error("Error updating Auto Deploy");
|
toast.error("Error updating Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -0,0 +1,367 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
databaseId: string;
|
||||||
|
databaseType: Exclude<ServiceType, "application" | "redis">;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RestoreBackupSchema = z.object({
|
||||||
|
destinationId: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please select a destination",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Destination is required",
|
||||||
|
}),
|
||||||
|
backupFile: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please select a backup file",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Backup file is required",
|
||||||
|
}),
|
||||||
|
databaseName: z
|
||||||
|
.string({
|
||||||
|
required_error: "Please enter a database name",
|
||||||
|
})
|
||||||
|
.min(1, {
|
||||||
|
message: "Database name is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
|
||||||
|
|
||||||
|
export const RestoreBackup = ({ databaseId, databaseType }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const { data: destinations = [] } = api.destination.all.useQuery();
|
||||||
|
|
||||||
|
const form = useForm<RestoreBackup>({
|
||||||
|
defaultValues: {
|
||||||
|
destinationId: "",
|
||||||
|
backupFile: "",
|
||||||
|
databaseName: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const destionationId = form.watch("destinationId");
|
||||||
|
|
||||||
|
const debouncedSetSearch = debounce((value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
||||||
|
{
|
||||||
|
destinationId: destionationId,
|
||||||
|
search,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isOpen && !!destionationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
|
||||||
|
// const { mutateAsync: restore, isLoading: isRestoring } =
|
||||||
|
// api.backup.restoreBackup.useMutation();
|
||||||
|
|
||||||
|
api.backup.restoreBackupWithLogs.useSubscription(
|
||||||
|
{
|
||||||
|
databaseId,
|
||||||
|
databaseType,
|
||||||
|
databaseName: form.watch("databaseName"),
|
||||||
|
backupFile: form.watch("backupFile"),
|
||||||
|
destinationId: form.watch("destinationId"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isDeploying,
|
||||||
|
onData(log) {
|
||||||
|
if (!isDrawerOpen) {
|
||||||
|
setIsDrawerOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log === "Restore completed successfully!") {
|
||||||
|
setIsDeploying(false);
|
||||||
|
}
|
||||||
|
const parsedLogs = parseLogs(log);
|
||||||
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error("Restore logs error:", error);
|
||||||
|
setIsDeploying(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = async (_data: RestoreBackup) => {
|
||||||
|
setIsDeploying(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
Restore Backup
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center">
|
||||||
|
<RotateCcw className="mr-2 size-4" />
|
||||||
|
Restore Backup
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select a destination and search for backup files
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-restore-backup"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="destinationId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="">
|
||||||
|
<FormLabel>Destination</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? destinations.find(
|
||||||
|
(d) => d.destinationId === field.value,
|
||||||
|
)?.name
|
||||||
|
: "Select Destination"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search destinations..."
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<CommandEmpty>No destinations found.</CommandEmpty>
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<CommandGroup>
|
||||||
|
{destinations.map((destination) => (
|
||||||
|
<CommandItem
|
||||||
|
value={destination.destinationId}
|
||||||
|
key={destination.destinationId}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue(
|
||||||
|
"destinationId",
|
||||||
|
destination.destinationId,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{destination.name}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
destination.destinationId === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="backupFile"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="">
|
||||||
|
<FormLabel className="flex items-center justify-between">
|
||||||
|
Search Backup Files
|
||||||
|
{field.value && (
|
||||||
|
<Badge variant="outline">
|
||||||
|
{field.value}
|
||||||
|
<Copy
|
||||||
|
className="ml-2 size-4 cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
copy(field.value);
|
||||||
|
toast.success("Backup file copied to clipboard");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between !bg-input",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value || "Search and select a backup file"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search backup files..."
|
||||||
|
onValueChange={debouncedSetSearch}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
Loading backup files...
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 && search ? (
|
||||||
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
No backup files found for "{search}"
|
||||||
|
</div>
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||||
|
No backup files available
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<CommandGroup>
|
||||||
|
{files.map((file) => (
|
||||||
|
<CommandItem
|
||||||
|
value={file}
|
||||||
|
key={file}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue("backupFile", file);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{file}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
file === field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="databaseName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="">
|
||||||
|
<FormLabel>Database Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Enter database name" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={isDeploying}
|
||||||
|
form="hook-form-restore-backup"
|
||||||
|
type="submit"
|
||||||
|
disabled={!form.watch("backupFile")}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<DrawerLogs
|
||||||
|
isOpen={isDrawerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
// refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ import { toast } from "sonner";
|
|||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { AddBackup } from "./add-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
import { UpdateBackup } from "./update-backup";
|
import { UpdateBackup } from "./update-backup";
|
||||||
|
import { RestoreBackup } from "./restore-backup";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -27,7 +28,9 @@ interface Props {
|
|||||||
type: Exclude<ServiceType, "application" | "redis">;
|
type: Exclude<ServiceType, "application" | "redis">;
|
||||||
}
|
}
|
||||||
export const ShowBackups = ({ id, type }: Props) => {
|
export const ShowBackups = ({ id, type }: Props) => {
|
||||||
const [activeManualBackup, setActiveManualBackup] = useState<string | undefined>();
|
const [activeManualBackup, setActiveManualBackup] = useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
@@ -69,7 +72,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||||
|
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
||||||
|
<RestoreBackup databaseId={id} databaseType={type} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
@@ -96,11 +102,14 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No backups configured
|
No backups configured
|
||||||
</span>
|
</span>
|
||||||
<AddBackup
|
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||||
databaseId={id}
|
<AddBackup
|
||||||
databaseType={type}
|
databaseId={id}
|
||||||
refetch={refetch}
|
databaseType={type}
|
||||||
/>
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
<RestoreBackup databaseId={id} databaseType={type} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
@@ -142,7 +151,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Keep Latest</span>
|
<span className="font-medium">Keep Latest</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{backup.keepLatestCount || 'All'}
|
{backup.keepLatestCount || "All"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +162,10 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
isLoading={isManualBackup && activeManualBackup === backup.backupId}
|
isLoading={
|
||||||
|
isManualBackup &&
|
||||||
|
activeManualBackup === backup.backupId
|
||||||
|
}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setActiveManualBackup(backup.backupId);
|
setActiveManualBackup(backup.backupId);
|
||||||
await manualBackup({
|
await manualBackup({
|
||||||
@@ -178,6 +190,7 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
<TooltipContent>Run Manual Backup</TooltipContent>
|
<TooltipContent>Run Manual Backup</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<UpdateBackup
|
<UpdateBackup
|
||||||
backupId={backup.backupId}
|
backupId={backup.backupId}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
const wsUrl = `${protocol}//${
|
const wsUrl = `${protocol}//${
|
||||||
window.location.host
|
window.location.host
|
||||||
}/docker-container-logs?${params.toString()}`;
|
}/docker-container-logs?${params.toString()}`;
|
||||||
console.log("Connecting to WebSocket:", wsUrl);
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
const resetNoDataTimeout = () => {
|
const resetNoDataTimeout = () => {
|
||||||
@@ -136,7 +135,6 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
ws.close();
|
ws.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("WebSocket connected");
|
|
||||||
resetNoDataTimeout();
|
resetNoDataTimeout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -19,141 +20,152 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
const { data, refetch } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mariadbId,
|
mariadbId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
<Form {...form}>
|
{!getIp && (
|
||||||
<form
|
<AlertBlock type="warning">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
You need to set an IP address in your{" "}
|
||||||
className="flex flex-col gap-4"
|
<Link
|
||||||
>
|
href="/dashboard/settings/server"
|
||||||
<div className="grid md:grid-cols-2 gap-4 ">
|
className="text-primary"
|
||||||
<div className="md:col-span-2 space-y-4">
|
>
|
||||||
<FormField
|
{data?.serverId
|
||||||
control={form.control}
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
name="externalPort"
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
render={({ field }) => {
|
</Link>{" "}
|
||||||
return (
|
to fix the database url connection.
|
||||||
<FormItem>
|
</AlertBlock>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
)}
|
||||||
<FormControl>
|
<Form {...form}>
|
||||||
<Input
|
<form
|
||||||
placeholder="3306"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
{...field}
|
className="flex flex-col gap-4"
|
||||||
value={field.value || ""}
|
>
|
||||||
/>
|
<div className="grid md:grid-cols-2 gap-4 ">
|
||||||
</FormControl>
|
<div className="md:col-span-2 space-y-4">
|
||||||
<FormMessage />
|
<FormField
|
||||||
</FormItem>
|
control={form.control}
|
||||||
);
|
name="externalPort"
|
||||||
}}
|
render={({ field }) => {
|
||||||
/>
|
return (
|
||||||
</div>
|
<FormItem>
|
||||||
</div>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{!!data?.externalPort && (
|
<FormControl>
|
||||||
<div className="grid w-full gap-8">
|
<Input
|
||||||
<div className="flex flex-col gap-3">
|
placeholder="3306"
|
||||||
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
{...field}
|
||||||
<Label>External Host</Label>
|
value={field.value || ""}
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
</div>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!!data?.externalPort && (
|
||||||
|
<div className="grid w-full gap-8">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{/* jdbc:mariadb://5.161.59.207:3306/pixel-calculate?user=mariadb&password=HdVXfq6hM7W7F1 */}
|
||||||
|
<Label>External Host</Label>
|
||||||
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,242 +8,245 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
const { data, refetch } = api.mariadb.one.useQuery(
|
||||||
{
|
{
|
||||||
mariadbId,
|
mariadbId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mariadbId },
|
{ enabled: !!mariadbId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.mariadb.reload.useMutation();
|
api.mariadb.reload.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.mariadb.start.useMutation();
|
api.mariadb.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.mariadb.stop.useMutation();
|
api.mariadb.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.mariadb.deployWithLogs.useSubscription(
|
api.mariadb.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Mariadb"
|
title="Deploy Mariadb"
|
||||||
description="Are you sure you want to deploy this mariadb?"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the MariaDB database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the MariaDB database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mariadb"
|
title="Reload Mariadb"
|
||||||
description="Are you sure you want to reload this mariadb?"
|
description="Are you sure you want to reload this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mariadb reloaded successfully");
|
toast.success("Mariadb reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mariadb");
|
toast.error("Error reloading Mariadb");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MariaDB service without rebuilding</p>
|
<p>Restart the MariaDB service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start Mariadb"
|
||||||
title="Start Mariadb"
|
description="Are you sure you want to start this mariadb?"
|
||||||
description="Are you sure you want to start this mariadb?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await start({
|
||||||
await start({
|
mariadbId: mariadbId,
|
||||||
mariadbId: mariadbId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Mariadb started successfully");
|
||||||
toast.success("Mariadb started successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error starting Mariadb");
|
||||||
toast.error("Error starting Mariadb");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the MariaDB database (requires a previous
|
||||||
Start the MariaDB database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Mariadb"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this mariadb?"
|
||||||
title="Stop Mariadb"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this mariadb?"
|
await stop({
|
||||||
onClick={async () => {
|
mariadbId: mariadbId,
|
||||||
await stop({
|
})
|
||||||
mariadbId: mariadbId,
|
.then(() => {
|
||||||
})
|
toast.success("Mariadb stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Mariadb stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Mariadb");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Mariadb");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running MariaDB database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running MariaDB database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the MariaDB container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -19,140 +20,151 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
const { data, refetch } = api.mongo.one.useQuery({ mongoId });
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mongoId,
|
mongoId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
<Form {...form}>
|
{!getIp && (
|
||||||
<form
|
<AlertBlock type="warning">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
You need to set an IP address in your{" "}
|
||||||
className="flex flex-col gap-4"
|
<Link
|
||||||
>
|
href="/dashboard/settings/server"
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
className="text-primary"
|
||||||
<div className="col-span-2 space-y-4">
|
>
|
||||||
<FormField
|
{data?.serverId
|
||||||
control={form.control}
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
name="externalPort"
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
render={({ field }) => {
|
</Link>{" "}
|
||||||
return (
|
to fix the database url connection.
|
||||||
<FormItem>
|
</AlertBlock>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
)}
|
||||||
<FormControl>
|
<Form {...form}>
|
||||||
<Input
|
<form
|
||||||
placeholder="27017"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
{...field}
|
className="flex flex-col gap-4"
|
||||||
value={field.value || ""}
|
>
|
||||||
/>
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
</FormControl>
|
<div className="col-span-2 space-y-4">
|
||||||
<FormMessage />
|
<FormField
|
||||||
</FormItem>
|
control={form.control}
|
||||||
);
|
name="externalPort"
|
||||||
}}
|
render={({ field }) => {
|
||||||
/>
|
return (
|
||||||
</div>
|
<FormItem>
|
||||||
</div>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{!!data?.externalPort && (
|
<FormControl>
|
||||||
<div className="grid w-full gap-8">
|
<Input
|
||||||
<div className="flex flex-col gap-3">
|
placeholder="27017"
|
||||||
<Label>External Host</Label>
|
{...field}
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
value={field.value || ""}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!!data?.externalPort && (
|
||||||
|
<div className="grid w-full gap-8">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label>External Host</Label>
|
||||||
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,246 +3,249 @@ import { DrawerLogs } from "@/components/shared/drawer-logs";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mongoId },
|
{ enabled: !!mongoId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.mongo.reload.useMutation();
|
api.mongo.reload.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.mongo.start.useMutation();
|
api.mongo.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.mongo.stop.useMutation();
|
api.mongo.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.mongo.deployWithLogs.useSubscription(
|
api.mongo.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to deploy this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the MongoDB database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the MongoDB database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mongo"
|
title="Reload Mongo"
|
||||||
description="Are you sure you want to reload this mongo?"
|
description="Are you sure you want to reload this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mongo reloaded successfully");
|
toast.success("Mongo reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mongo");
|
toast.error("Error reloading Mongo");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MongoDB service without rebuilding</p>
|
<p>Restart the MongoDB service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start Mongo"
|
||||||
title="Start Mongo"
|
description="Are you sure you want to start this mongo?"
|
||||||
description="Are you sure you want to start this mongo?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await start({
|
||||||
await start({
|
mongoId: mongoId,
|
||||||
mongoId: mongoId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Mongo started successfully");
|
||||||
toast.success("Mongo started successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error starting Mongo");
|
||||||
toast.error("Error starting Mongo");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the MongoDB database (requires a previous
|
||||||
Start the MongoDB database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Mongo"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this mongo?"
|
||||||
title="Stop Mongo"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this mongo?"
|
await stop({
|
||||||
onClick={async () => {
|
mongoId: mongoId,
|
||||||
await stop({
|
})
|
||||||
mongoId: mongoId,
|
.then(() => {
|
||||||
})
|
toast.success("Mongo stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Mongo stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Mongo");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Mongo");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running MongoDB database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running MongoDB database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the MongoDB container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Used: {currentData.cpu.value}%
|
Used: {currentData.cpu.value}
|
||||||
</span>
|
</span>
|
||||||
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
||||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -19,140 +20,151 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
const { data, refetch } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
mysqlId,
|
mysqlId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
data?.databaseUser,
|
data?.databaseUser,
|
||||||
form,
|
form,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
<Form {...form}>
|
{!getIp && (
|
||||||
<form
|
<AlertBlock type="warning">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
You need to set an IP address in your{" "}
|
||||||
className="flex flex-col gap-4"
|
<Link
|
||||||
>
|
href="/dashboard/settings/server"
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
className="text-primary"
|
||||||
<div className="col-span-2 space-y-4">
|
>
|
||||||
<FormField
|
{data?.serverId
|
||||||
control={form.control}
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
name="externalPort"
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
render={({ field }) => {
|
</Link>{" "}
|
||||||
return (
|
to fix the database url connection.
|
||||||
<FormItem>
|
</AlertBlock>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
)}
|
||||||
<FormControl>
|
<Form {...form}>
|
||||||
<Input
|
<form
|
||||||
placeholder="3306"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
{...field}
|
className="flex flex-col gap-4"
|
||||||
value={field.value || ""}
|
>
|
||||||
/>
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
</FormControl>
|
<div className="col-span-2 space-y-4">
|
||||||
<FormMessage />
|
<FormField
|
||||||
</FormItem>
|
control={form.control}
|
||||||
);
|
name="externalPort"
|
||||||
}}
|
render={({ field }) => {
|
||||||
/>
|
return (
|
||||||
</div>
|
<FormItem>
|
||||||
</div>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{!!data?.externalPort && (
|
<FormControl>
|
||||||
<div className="grid w-full gap-8">
|
<Input
|
||||||
<div className="flex flex-col gap-3">
|
placeholder="3306"
|
||||||
<Label>External Host</Label>
|
{...field}
|
||||||
<ToggleVisibilityInput disabled value={connectionUrl} />
|
value={field.value || ""}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!!data?.externalPort && (
|
||||||
|
<div className="grid w-full gap-8">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label>External Host</Label>
|
||||||
|
<ToggleVisibilityInput disabled value={connectionUrl} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,239 +8,242 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||||
const { data, refetch } = api.mysql.one.useQuery(
|
const { data, refetch } = api.mysql.one.useQuery(
|
||||||
{
|
{
|
||||||
mysqlId,
|
mysqlId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mysqlId },
|
{ enabled: !!mysqlId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.mysql.reload.useMutation();
|
api.mysql.reload.useMutation();
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.mysql.start.useMutation();
|
api.mysql.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.mysql.stop.useMutation();
|
api.mysql.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.mysql.deployWithLogs.useSubscription(
|
api.mysql.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Mysql"
|
title="Deploy Mysql"
|
||||||
description="Are you sure you want to deploy this mysql?"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the MySQL database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the MySQL database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mysql"
|
title="Reload Mysql"
|
||||||
description="Are you sure you want to reload this mysql?"
|
description="Are you sure you want to reload this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mysql reloaded successfully");
|
toast.success("Mysql reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mysql");
|
toast.error("Error reloading Mysql");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MySQL service without rebuilding</p>
|
<p>Restart the MySQL service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start Mysql"
|
||||||
title="Start Mysql"
|
description="Are you sure you want to start this mysql?"
|
||||||
description="Are you sure you want to start this mysql?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await start({
|
||||||
await start({
|
mysqlId: mysqlId,
|
||||||
mysqlId: mysqlId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Mysql started successfully");
|
||||||
toast.success("Mysql started successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error starting Mysql");
|
||||||
toast.error("Error starting Mysql");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the MySQL database (requires a previous
|
||||||
Start the MySQL database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Mysql"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this mysql?"
|
||||||
title="Stop Mysql"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this mysql?"
|
await stop({
|
||||||
onClick={async () => {
|
mysqlId: mysqlId,
|
||||||
await stop({
|
})
|
||||||
mysqlId: mysqlId,
|
.then(() => {
|
||||||
})
|
toast.success("Mysql stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Mysql stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Mysql");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Mysql");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running MySQL database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running MySQL database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the MySQL container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -19,142 +20,153 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
const { data, refetch } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.postgres.saveExternalPort.useMutation();
|
api.postgres.saveExternalPort.useMutation();
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
postgresId,
|
postgresId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [
|
}, [
|
||||||
data?.appName,
|
data?.appName,
|
||||||
data?.externalPort,
|
data?.externalPort,
|
||||||
data?.databasePassword,
|
data?.databasePassword,
|
||||||
form,
|
form,
|
||||||
data?.databaseName,
|
data?.databaseName,
|
||||||
getIp,
|
getIp,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
<Form {...form}>
|
{!getIp && (
|
||||||
<form
|
<AlertBlock type="warning">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
You need to set an IP address in your{" "}
|
||||||
className="flex flex-col gap-4"
|
<Link
|
||||||
>
|
href="/dashboard/settings/server"
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
className="text-primary"
|
||||||
<div className="col-span-2 space-y-4">
|
>
|
||||||
<FormField
|
{data?.serverId
|
||||||
control={form.control}
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
name="externalPort"
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
render={({ field }) => {
|
</Link>{" "}
|
||||||
return (
|
to fix the database url connection.
|
||||||
<FormItem>
|
</AlertBlock>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
)}
|
||||||
<FormControl>
|
<Form {...form}>
|
||||||
<Input
|
<form
|
||||||
placeholder="5432"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
{...field}
|
className="flex flex-col gap-4"
|
||||||
value={field.value || ""}
|
>
|
||||||
/>
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
</FormControl>
|
<div className="col-span-2 space-y-4">
|
||||||
<FormMessage />
|
<FormField
|
||||||
</FormItem>
|
control={form.control}
|
||||||
);
|
name="externalPort"
|
||||||
}}
|
render={({ field }) => {
|
||||||
/>
|
return (
|
||||||
</div>
|
<FormItem>
|
||||||
</div>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{!!data?.externalPort && (
|
<FormControl>
|
||||||
<div className="grid w-full gap-8">
|
<Input
|
||||||
<div className="flex flex-col gap-3">
|
placeholder="5432"
|
||||||
<Label>External Host</Label>
|
{...field}
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
value={field.value || ""}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!!data?.externalPort && (
|
||||||
|
<div className="grid w-full gap-8">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label>External Host</Label>
|
||||||
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,15 +8,9 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
@@ -78,7 +72,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider disableHoverableContent={false}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Postgres"
|
title="Deploy Postgres"
|
||||||
description="Are you sure you want to deploy this postgres?"
|
description="Are you sure you want to deploy this postgres?"
|
||||||
@@ -89,23 +83,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the PostgreSQL database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the PostgreSQL database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Postgres"
|
title="Reload Postgres"
|
||||||
@@ -125,24 +119,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the PostgreSQL service without rebuilding</p>
|
<p>Reload the PostgreSQL without rebuilding it</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -162,27 +155,26 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStarting}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isStarting}
|
||||||
Start
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<CheckCircle2 className="size-4" />
|
>
|
||||||
<Tooltip>
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Start
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the PostgreSQL database (requires a previous
|
Start the PostgreSQL database (requires a previous
|
||||||
successful setup)
|
successful setup)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -201,24 +193,23 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="destructive"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isStopping}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="destructive"
|
||||||
>
|
isLoading={isStopping}
|
||||||
Stop
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Ban className="size-4" />
|
>
|
||||||
<Tooltip>
|
<Ban className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Stop
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running PostgreSQL database</p>
|
<p>Stop the currently running PostgreSQL database</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -226,8 +217,11 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
>
|
>
|
||||||
<Button variant="outline">
|
<Button
|
||||||
<Terminal />
|
variant="outline"
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
|
|||||||
@@ -5,58 +5,58 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>User</Label>
|
<Label>User</Label>
|
||||||
<Input disabled value={data?.databaseUser} />
|
<Input disabled value={data?.databaseUser} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Database Name</Label>
|
<Label>Database Name</Label>
|
||||||
<Input disabled value={data?.databaseName} />
|
<Input disabled value={data?.databaseName} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Password</Label>
|
<Label>Password</Label>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
value={data?.databasePassword}
|
value={data?.databasePassword}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Port (Container)</Label>
|
<Label>Internal Port (Container)</Label>
|
||||||
<Input disabled value="5432" />
|
<Input disabled value="5432" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Host</Label>
|
<Label>Internal Host</Label>
|
||||||
<Input disabled value={data?.appName} />
|
<Input disabled value={data?.appName} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
value={`postgresql://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5432/${data?.databaseName}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w
|
// ReplyError: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-w
|
||||||
|
|||||||
@@ -21,145 +21,146 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon } from "lucide-react";
|
import { PenBox } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const updatePostgresSchema = z.object({
|
const updatePostgresSchema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
|
type UpdatePostgres = z.infer<typeof updatePostgresSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdatePostgres = ({ postgresId }: Props) => {
|
export const UpdatePostgres = ({ postgresId }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
const { mutateAsync, error, isError, isLoading } =
|
||||||
api.postgres.update.useMutation();
|
api.postgres.update.useMutation();
|
||||||
const { data } = api.postgres.one.useQuery(
|
const { data } = api.postgres.one.useQuery(
|
||||||
{
|
{
|
||||||
postgresId,
|
postgresId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!postgresId,
|
enabled: !!postgresId,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
const form = useForm<UpdatePostgres>({
|
const form = useForm<UpdatePostgres>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
description: data?.description ?? "",
|
description: data?.description ?? "",
|
||||||
name: data?.name ?? "",
|
name: data?.name ?? "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(updatePostgresSchema),
|
resolver: zodResolver(updatePostgresSchema),
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
description: data.description ?? "",
|
description: data.description ?? "",
|
||||||
name: data.name,
|
name: data.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form, form.reset]);
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: UpdatePostgres) => {
|
const onSubmit = async (formData: UpdatePostgres) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Postgres updated successfully");
|
toast.success("Postgres updated successfully");
|
||||||
utils.postgres.one.invalidate({
|
utils.postgres.one.invalidate({
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating Postgres");
|
toast.error("Error updating Postgres");
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-blue-500/10 "
|
className="group hover:bg-blue-500/10 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBox className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Modify Postgres</DialogTitle>
|
<DialogTitle>Modify Postgres</DialogTitle>
|
||||||
<DialogDescription>Update the Postgres data</DialogDescription>
|
<DialogDescription>Update the Postgres data</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid items-center gap-4">
|
<div className="grid items-center gap-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
id="hook-form-update-postgres"
|
id="hook-form-update-postgres"
|
||||||
className="grid w-full gap-4 "
|
className="grid w-full gap-4 "
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Description about your project..."
|
placeholder="Description about your project..."
|
||||||
className="resize-none"
|
className="resize-none"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-update-postgres"
|
form="hook-form-update-postgres"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Update
|
>
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
</form>
|
</DialogFooter>
|
||||||
</Form>
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</div>
|
||||||
</Dialog>
|
</DialogContent>
|
||||||
);
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${customBaseUrl || "https://dokploy.github.io/templates"}/blueprints/${template.id}/${template.logo}`}
|
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template.id}/${template.logo}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"object-contain",
|
"object-contain",
|
||||||
viewMode === "detailed" ? "size-24" : "size-16",
|
viewMode === "detailed" ? "size-24" : "size-16",
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const ShowProjects = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 flex-wrap gap-5">
|
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
|
||||||
{filteredProjects?.map((project) => {
|
{filteredProjects?.map((project) => {
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
project?.mariadb.length === 0 &&
|
project?.mariadb.length === 0 &&
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -19,134 +20,145 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
const DockerProviderSchema = z.object({
|
||||||
externalPort: z.preprocess((a) => {
|
externalPort: z.preprocess((a) => {
|
||||||
if (a !== null) {
|
if (a !== null) {
|
||||||
const parsed = Number.parseInt(z.string().parse(a), 10);
|
const parsed = Number.parseInt(z.string().parse(a), 10);
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
return Number.isNaN(parsed) ? null : parsed;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, z
|
}, z.number().gte(0, "Range must be 0 - 65535").lte(65535, "Range must be 0 - 65535").nullable()),
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
const { data, refetch } = api.redis.one.useQuery({ redisId });
|
||||||
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveExternalPort.useMutation();
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
const [connectionUrl, setConnectionUrl] = useState("");
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
const getIp = data?.server?.ipAddress || ip;
|
||||||
|
|
||||||
const form = useForm<DockerProvider>({
|
const form = useForm<DockerProvider>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
resolver: zodResolver(DockerProviderSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.externalPort) {
|
if (data?.externalPort) {
|
||||||
form.reset({
|
form.reset({
|
||||||
externalPort: data.externalPort,
|
externalPort: data.externalPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
externalPort: values.externalPort,
|
externalPort: values.externalPort,
|
||||||
redisId,
|
redisId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("External Port updated");
|
toast.success("External Port updated");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error saving the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const _hostname = window.location.hostname;
|
const _hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
}, [data?.appName, data?.externalPort, data?.databasePassword, form, getIp]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
In order to make the database reachable trought internet is
|
In order to make the database reachable trought internet is
|
||||||
required to set a port, make sure the port is not used by another
|
required to set a port, make sure the port is not used by another
|
||||||
application or database
|
application or database
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
<CardContent className="flex w-full flex-col gap-4">
|
||||||
<Form {...form}>
|
{!getIp && (
|
||||||
<form
|
<AlertBlock type="warning">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
You need to set an IP address in your{" "}
|
||||||
className="flex flex-col gap-4"
|
<Link
|
||||||
>
|
href="/dashboard/settings/server"
|
||||||
<div className="grid grid-cols-2 gap-4 ">
|
className="text-primary"
|
||||||
<div className="col-span-2 space-y-4">
|
>
|
||||||
<FormField
|
{data?.serverId
|
||||||
control={form.control}
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
name="externalPort"
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
render={({ field }) => {
|
</Link>{" "}
|
||||||
return (
|
to fix the database url connection.
|
||||||
<FormItem>
|
</AlertBlock>
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
)}
|
||||||
<FormControl>
|
<Form {...form}>
|
||||||
<Input
|
<form
|
||||||
placeholder="6379"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
{...field}
|
className="flex flex-col gap-4"
|
||||||
value={field.value || ""}
|
>
|
||||||
/>
|
<div className="grid grid-cols-2 gap-4 ">
|
||||||
</FormControl>
|
<div className="col-span-2 space-y-4">
|
||||||
<FormMessage />
|
<FormField
|
||||||
</FormItem>
|
control={form.control}
|
||||||
);
|
name="externalPort"
|
||||||
}}
|
render={({ field }) => {
|
||||||
/>
|
return (
|
||||||
</div>
|
<FormItem>
|
||||||
</div>
|
<FormLabel>External Port (Internet)</FormLabel>
|
||||||
{!!data?.externalPort && (
|
<FormControl>
|
||||||
<div className="grid w-full gap-8">
|
<Input
|
||||||
<div className="flex flex-col gap-3">
|
placeholder="6379"
|
||||||
<Label>External Host</Label>
|
{...field}
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
value={field.value || ""}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</FormControl>
|
||||||
)}
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!!data?.externalPort && (
|
||||||
|
<div className="grid w-full gap-8">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label>External Host</Label>
|
||||||
|
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button type="submit" isLoading={isLoading}>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,241 +8,244 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
Ban,
|
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||||
CheckCircle2,
|
|
||||||
HelpCircle,
|
|
||||||
RefreshCcw,
|
|
||||||
Terminal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||||
const { data, refetch } = api.redis.one.useQuery(
|
const { data, refetch } = api.redis.one.useQuery(
|
||||||
{
|
{
|
||||||
redisId,
|
redisId,
|
||||||
},
|
},
|
||||||
{ enabled: !!redisId },
|
{ enabled: !!redisId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.redis.reload.useMutation();
|
api.redis.reload.useMutation();
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.redis.start.useMutation();
|
api.redis.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.redis.stop.useMutation();
|
api.redis.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.redis.deployWithLogs.useSubscription(
|
api.redis.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
redisId: redisId,
|
redisId: redisId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Redis"
|
title="Deploy Redis"
|
||||||
description="Are you sure you want to deploy this redis?"
|
description="Are you sure you want to deploy this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="default"
|
||||||
>
|
isLoading={data?.applicationStatus === "running"}
|
||||||
Deploy
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
<Rocket className="size-4 mr-1" />
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
Deploy
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the Redis database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the Redis database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</Button>
|
</Tooltip>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Redis"
|
title="Reload Redis"
|
||||||
description="Are you sure you want to reload this redis?"
|
description="Are you sure you want to reload this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await reload({
|
||||||
redisId: redisId,
|
redisId: redisId,
|
||||||
appName: data?.appName || "",
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Redis reloaded successfully");
|
toast.success("Redis reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Redis");
|
toast.error("Error reloading Redis");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<Button
|
||||||
className="flex items-center gap-1.5"
|
variant="secondary"
|
||||||
>
|
isLoading={isReloading}
|
||||||
Reload
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<RefreshCcw className="size-4" />
|
>
|
||||||
<Tooltip>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipTrigger asChild>
|
Reload
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the Redis service without rebuilding</p>
|
<p>Restart the Redis service without rebuilding</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</DialogAction>
|
||||||
</DialogAction>
|
{data?.applicationStatus === "idle" ? (
|
||||||
{data?.applicationStatus === "idle" ? (
|
<DialogAction
|
||||||
<DialogAction
|
title="Start Redis"
|
||||||
title="Start Redis"
|
description="Are you sure you want to start this redis?"
|
||||||
description="Are you sure you want to start this redis?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await start({
|
||||||
await start({
|
redisId: redisId,
|
||||||
redisId: redisId,
|
})
|
||||||
})
|
.then(() => {
|
||||||
.then(() => {
|
toast.success("Redis started successfully");
|
||||||
toast.success("Redis started successfully");
|
refetch();
|
||||||
refetch();
|
})
|
||||||
})
|
.catch(() => {
|
||||||
.catch(() => {
|
toast.error("Error starting Redis");
|
||||||
toast.error("Error starting Redis");
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="secondary"
|
<Button
|
||||||
isLoading={isStarting}
|
variant="secondary"
|
||||||
className="flex items-center gap-1.5"
|
isLoading={isStarting}
|
||||||
>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
<CheckCircle2 className="size-4" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<Tooltip>
|
Start
|
||||||
<TooltipTrigger asChild>
|
</Button>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<p>
|
||||||
<p>
|
Start the Redis database (requires a previous
|
||||||
Start the Redis database (requires a previous
|
successful setup)
|
||||||
successful setup)
|
</p>
|
||||||
</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
</Tooltip>
|
</DialogAction>
|
||||||
</Button>
|
) : (
|
||||||
</DialogAction>
|
<DialogAction
|
||||||
) : (
|
title="Stop Redis"
|
||||||
<DialogAction
|
description="Are you sure you want to stop this redis?"
|
||||||
title="Stop Redis"
|
onClick={async () => {
|
||||||
description="Are you sure you want to stop this redis?"
|
await stop({
|
||||||
onClick={async () => {
|
redisId: redisId,
|
||||||
await stop({
|
})
|
||||||
redisId: redisId,
|
.then(() => {
|
||||||
})
|
toast.success("Redis stopped successfully");
|
||||||
.then(() => {
|
refetch();
|
||||||
toast.success("Redis stopped successfully");
|
})
|
||||||
refetch();
|
.catch(() => {
|
||||||
})
|
toast.error("Error stopping Redis");
|
||||||
.catch(() => {
|
});
|
||||||
toast.error("Error stopping Redis");
|
}}
|
||||||
});
|
>
|
||||||
}}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
isLoading={isStopping}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Stop
|
<Ban className="size-4 mr-1" />
|
||||||
<Ban className="size-4" />
|
Stop
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipTrigger>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<TooltipPrimitive.Portal>
|
<p>Stop the currently running Redis database</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>Stop the currently running Redis database</p>
|
</TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</TooltipPrimitive.Portal>
|
</DialogAction>
|
||||||
</Tooltip>
|
)}
|
||||||
</Button>
|
</TooltipProvider>
|
||||||
</DialogAction>
|
<DockerTerminalModal
|
||||||
)}
|
appName={data?.appName || ""}
|
||||||
</TooltipProvider>
|
serverId={data?.serverId || ""}
|
||||||
<DockerTerminalModal
|
>
|
||||||
appName={data?.appName || ""}
|
<Tooltip>
|
||||||
serverId={data?.serverId || ""}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Button variant="outline">
|
variant="outline"
|
||||||
<Terminal />
|
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Terminal className="size-4" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the Redis container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</DockerTerminalModal>
|
||||||
}}
|
</CardContent>
|
||||||
filteredLogs={filteredLogs}
|
</Card>
|
||||||
/>
|
<DrawerLogs
|
||||||
</div>
|
isOpen={isDrawerOpen}
|
||||||
</>
|
onClose={() => {
|
||||||
);
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Tesla" {...field} />
|
<Input placeholder="Vandelay Industries" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import Link from "next/link";
|
|||||||
import { AddManager } from "./manager/add-manager";
|
import { AddManager } from "./manager/add-manager";
|
||||||
import { AddWorker } from "./workers/add-worker";
|
import { AddWorker } from "./workers/add-worker";
|
||||||
|
|
||||||
export const AddNode = () => {
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddNode = ({ serverId }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -53,10 +57,10 @@ export const AddNode = () => {
|
|||||||
<TabsTrigger value="manager">Manager</TabsTrigger>
|
<TabsTrigger value="manager">Manager</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="worker" className="pt-4">
|
<TabsContent value="worker" className="pt-4">
|
||||||
<AddWorker />
|
<AddWorker serverId={serverId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="manager" className="pt-4">
|
<TabsContent value="manager" className="pt-4">
|
||||||
<AddManager />
|
<AddManager serverId={serverId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
|
|||||||
import { CopyIcon } from "lucide-react";
|
import { CopyIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const AddManager = () => {
|
interface Props {
|
||||||
const { data } = api.cluster.addManager.useQuery();
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddManager = ({ serverId }: Props) => {
|
||||||
|
const { data } = api.cluster.addManager.useQuery({ serverId });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowNodes } from "./show-nodes";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowNodesModal = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Show Nodes
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
||||||
|
<div className="grid w-full gap-1">
|
||||||
|
<ShowNodes serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -32,13 +32,25 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Boxes, HelpCircle, LockIcon, MoreHorizontal } from "lucide-react";
|
import {
|
||||||
|
Boxes,
|
||||||
|
HelpCircle,
|
||||||
|
LockIcon,
|
||||||
|
MoreHorizontal,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddNode } from "./add-node";
|
import { AddNode } from "./add-node";
|
||||||
import { ShowNodeData } from "./show-node-data";
|
import { ShowNodeData } from "./show-node-data";
|
||||||
|
|
||||||
export const ShowNodes = () => {
|
interface Props {
|
||||||
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery();
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowNodes = ({ serverId }: Props) => {
|
||||||
|
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
const { data: registry } = api.registry.all.useQuery();
|
const { data: registry } = api.registry.all.useQuery();
|
||||||
|
|
||||||
const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation();
|
const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation();
|
||||||
@@ -58,14 +70,17 @@ export const ShowNodes = () => {
|
|||||||
</div>
|
</div>
|
||||||
{haveAtLeastOneRegistry && (
|
{haveAtLeastOneRegistry && (
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<AddNode />
|
<AddNode serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t min-h-[35vh]">
|
<CardContent className="space-y-2 py-8 border-t min-h-[35vh]">
|
||||||
{haveAtLeastOneRegistry ? (
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center w-full h-[40vh]">
|
||||||
|
<Loader2 className="size-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : haveAtLeastOneRegistry ? (
|
||||||
<div className="grid md:grid-cols-1 gap-4">
|
<div className="grid md:grid-cols-1 gap-4">
|
||||||
{isLoading && <div>Loading...</div>}
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableCaption>
|
<TableCaption>
|
||||||
A list of your managers / workers.
|
A list of your managers / workers.
|
||||||
@@ -137,6 +152,7 @@ export const ShowNodes = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deleteNode({
|
await deleteNode({
|
||||||
nodeId: node.ID,
|
nodeId: node.ID,
|
||||||
|
serverId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
|
|||||||
import { CopyIcon } from "lucide-react";
|
import { CopyIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const AddWorker = () => {
|
interface Props {
|
||||||
const { data } = api.cluster.addWorker.useQuery();
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddWorker = ({ serverId }: Props) => {
|
||||||
|
const { data } = api.cluster.addWorker.useQuery({ serverId });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ export const Enable2FA = () => {
|
|||||||
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||||
setIsPasswordLoading(true);
|
setIsPasswordLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data: enableData } = await authClient.twoFactor.enable({
|
const { data: enableData, error } = await authClient.twoFactor.enable({
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!enableData) {
|
if (!enableData) {
|
||||||
throw new Error("No data received from server");
|
throw new Error(error?.message || "Error enabling 2FA");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableData.backupCodes) {
|
if (enableData.backupCodes) {
|
||||||
@@ -95,7 +95,8 @@ export const Enable2FA = () => {
|
|||||||
error instanceof Error ? error.message : "Error setting up 2FA",
|
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||||
);
|
);
|
||||||
passwordForm.setError("password", {
|
passwordForm.setError("password", {
|
||||||
message: "Error verifying password",
|
message:
|
||||||
|
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsPasswordLoading(false);
|
setIsPasswordLoading(false);
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Traefik Reloaded");
|
toast.success("Traefik Reloaded");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {});
|
||||||
toast.error("Error reloading Traefik");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
|
|||||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
|
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
@@ -328,6 +329,9 @@ export const ShowServers = () => {
|
|||||||
<ShowSwarmOverviewModal
|
<ShowSwarmOverviewModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
<ShowNodesModal
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -23,6 +24,7 @@ import { Loader2 } from "lucide-react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { badgeStateColor } from "../../application/logs/show";
|
||||||
|
|
||||||
const Terminal = dynamic(
|
const Terminal = dynamic(
|
||||||
() =>
|
() =>
|
||||||
@@ -109,7 +111,10 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
|||||||
key={container.containerId}
|
key={container.containerId}
|
||||||
value={container.containerId}
|
value={container.containerId}
|
||||||
>
|
>
|
||||||
{container.name} ({container.containerId}) {container.state}
|
{container.name} ({container.containerId}){" "}
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
||||||
@@ -44,7 +37,6 @@ interface Props {
|
|||||||
const PortSchema = z.object({
|
const PortSchema = z.object({
|
||||||
targetPort: z.number().min(1, "Target port is required"),
|
targetPort: z.number().min(1, "Target port is required"),
|
||||||
publishedPort: z.number().min(1, "Published port is required"),
|
publishedPort: z.number().min(1, "Published port is required"),
|
||||||
publishMode: z.enum(["ingress", "host"]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const TraefikPortsSchema = z.object({
|
const TraefikPortsSchema = z.object({
|
||||||
@@ -88,7 +80,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
}, [currentPorts, form]);
|
}, [currentPorts, form]);
|
||||||
|
|
||||||
const handleAddPort = () => {
|
const handleAddPort = () => {
|
||||||
append({ targetPort: 0, publishedPort: 0, publishMode: "host" });
|
append({ targetPort: 0, publishedPort: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: TraefikPortsForm) => {
|
const onSubmit = async (data: TraefikPortsForm) => {
|
||||||
@@ -99,9 +91,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (_error) {
|
} catch (_error) {}
|
||||||
toast.error(t("settings.server.webServer.traefik.portsUpdateError"));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -154,7 +144,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<Card key={field.id}>
|
<Card key={field.id}>
|
||||||
<CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent">
|
<CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`ports.${index}.targetPort`}
|
name={`ports.${index}.targetPort`}
|
||||||
@@ -207,39 +197,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={`ports.${index}.publishMode`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-sm font-medium text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"settings.server.webServer.traefik.publishMode",
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger className="dark:bg-black">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="host">
|
|
||||||
Host Mode
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="ingress">
|
|
||||||
Ingress Mode
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
@@ -263,30 +220,23 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
<strong>
|
<strong>
|
||||||
Each port mapping defines how external traffic reaches
|
Each port mapping defines how external traffic reaches
|
||||||
your containers.
|
your containers through Traefik.
|
||||||
</strong>
|
</strong>
|
||||||
<ul className="pt-2">
|
<ul className="pt-2">
|
||||||
<li>
|
<li>
|
||||||
<strong>Host Mode:</strong> Directly binds the port
|
<strong>Target Port:</strong> The port inside your
|
||||||
to the host machine.
|
container that the service is listening on.
|
||||||
<ul className="p-2 list-inside list-disc">
|
|
||||||
<li>
|
|
||||||
Best for single-node deployments or when you
|
|
||||||
need guaranteed port availability.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Ingress Mode:</strong> Routes through Docker
|
<strong>Published Port:</strong> The port on your
|
||||||
Swarm's load balancer.
|
host machine that will be mapped to the target port.
|
||||||
<ul className="p-2 list-inside list-disc">
|
|
||||||
<li>
|
|
||||||
Recommended for multi-node deployments and
|
|
||||||
better scalability.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p className="mt-2">
|
||||||
|
All ports are bound directly to the host machine,
|
||||||
|
allowing Traefik to handle incoming traffic and route
|
||||||
|
it appropriately to your services.
|
||||||
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { Loader2 } from "lucide-react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { badgeStateColor } from "../../application/logs/show";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
export const DockerLogsId = dynamic(
|
export const DockerLogsId = dynamic(
|
||||||
() =>
|
() =>
|
||||||
@@ -90,7 +92,10 @@ export const ShowModalLogs = ({
|
|||||||
key={container.containerId}
|
key={container.containerId}
|
||||||
value={container.containerId}
|
value={container.containerId}
|
||||||
>
|
>
|
||||||
{container.name} ({container.containerId}) {container.state}
|
{container.name} ({container.containerId}){" "}
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4">
|
<div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
|
||||||
{nodes.map((node) => (
|
{nodes.map((node) => (
|
||||||
<NodeCard key={node.ID} node={node} serverId={serverId} />
|
<NodeCard key={node.ID} node={node} serverId={serverId} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -908,7 +908,7 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Settings</SidebarGroupLabel>
|
<SidebarGroupLabel>Settings</SidebarGroupLabel>
|
||||||
<SidebarMenu className="gap-2">
|
<SidebarMenu className="gap-1">
|
||||||
{filteredSettings.map((item) => {
|
{filteredSettings.map((item) => {
|
||||||
const isSingle = item.isSingle !== false;
|
const isSingle = item.isSingle !== false;
|
||||||
const isActive = isSingle
|
const isActive = isSingle
|
||||||
@@ -1068,7 +1068,7 @@ export default function Page({ children }: Props) {
|
|||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col w-full gap-4 p-4 pt-0">{children}</div>
|
<div className="flex flex-col w-full p-4 pt-0">{children}</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
className={cn("text-left", className)}
|
className={cn("text-left", className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
value={props.value === undefined ? undefined : String(props.value)}
|
value={props.value === undefined || props.value === "" ? "" : String(props.value)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
@@ -60,6 +60,21 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
// If input is empty, make 0 when focus is lost
|
||||||
|
if (e.target.value === "") {
|
||||||
|
const syntheticEvent = {
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
value: "0",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
props.onChange?.(
|
||||||
|
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0078_uneven_omega_sentinel.sql
Normal file
1
apps/dokploy/drizzle/0078_uneven_omega_sentinel.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "cleanCache" boolean DEFAULT false;
|
||||||
5163
apps/dokploy/drizzle/meta/0078_snapshot.json
Normal file
5163
apps/dokploy/drizzle/meta/0078_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -547,6 +547,13 @@
|
|||||||
"when": 1741510086231,
|
"when": 1741510086231,
|
||||||
"tag": "0077_chemical_dreadnoughts",
|
"tag": "0077_chemical_dreadnoughts",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 78,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1742112194375,
|
||||||
|
"tag": "0078_uneven_omega_sentinel",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.20.2",
|
"version": "v0.20.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"adm-zip": "^0.5.14",
|
"adm-zip": "^0.5.14",
|
||||||
"ai": "^4.0.23",
|
"ai": "^4.0.23",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "1.2.0",
|
"better-auth": "1.2.4",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
"bullmq": "5.4.2",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export default async function handler(
|
|||||||
try {
|
try {
|
||||||
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
|
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
const owner = githubBody?.repository?.owner?.name;
|
const owner = githubBody?.repository?.owner?.name;
|
||||||
@@ -107,6 +108,7 @@ export default async function handler(
|
|||||||
eq(applications.branch, branchName),
|
eq(applications.branch, branchName),
|
||||||
eq(applications.repository, repository),
|
eq(applications.repository, repository),
|
||||||
eq(applications.owner, owner),
|
eq(applications.owner, owner),
|
||||||
|
eq(applications.githubId, githubResult.githubId),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,6 +153,7 @@ export default async function handler(
|
|||||||
eq(compose.branch, branchName),
|
eq(compose.branch, branchName),
|
||||||
eq(compose.repository, repository),
|
eq(compose.repository, repository),
|
||||||
eq(compose.owner, owner),
|
eq(compose.owner, owner),
|
||||||
|
eq(compose.githubId, githubResult.githubId),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,6 +243,7 @@ export default async function handler(
|
|||||||
eq(applications.branch, branch),
|
eq(applications.branch, branch),
|
||||||
eq(applications.isPreviewDeploymentsActive, true),
|
eq(applications.isPreviewDeploymentsActive, true),
|
||||||
eq(applications.owner, owner),
|
eq(applications.owner, owner),
|
||||||
|
eq(applications.githubId, githubResult.githubId),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
previewDeployments: true,
|
previewDeployments: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"settings.common.save": "Save",
|
"settings.common.save": "Save",
|
||||||
"settings.common.enterTerminal": "Enter the terminal",
|
"settings.common.enterTerminal": "Terminal",
|
||||||
"settings.server.domain.title": "Server Domain",
|
"settings.server.domain.title": "Server Domain",
|
||||||
"settings.server.domain.description": "Add a domain to your server application.",
|
"settings.server.domain.description": "Add a domain to your server application.",
|
||||||
"settings.server.domain.form.domain": "Domain",
|
"settings.server.domain.form.domain": "Domain",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"settings.server.webServer.description": "Reload or clean the web server.",
|
"settings.server.webServer.description": "Reload or clean the web server.",
|
||||||
"settings.server.webServer.actions": "Actions",
|
"settings.server.webServer.actions": "Actions",
|
||||||
"settings.server.webServer.reload": "Reload",
|
"settings.server.webServer.reload": "Reload",
|
||||||
"settings.server.webServer.watchLogs": "Watch logs",
|
"settings.server.webServer.watchLogs": "View Logs",
|
||||||
"settings.server.webServer.updateServerIp": "Update Server IP",
|
"settings.server.webServer.updateServerIp": "Update Server IP",
|
||||||
"settings.server.webServer.server.label": "Server",
|
"settings.server.webServer.server.label": "Server",
|
||||||
"settings.server.webServer.traefik.label": "Traefik",
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ import {
|
|||||||
createBackup,
|
createBackup,
|
||||||
findBackupById,
|
findBackupById,
|
||||||
findMariadbByBackupId,
|
findMariadbByBackupId,
|
||||||
|
findMariadbById,
|
||||||
findMongoByBackupId,
|
findMongoByBackupId,
|
||||||
|
findMongoById,
|
||||||
findMySqlByBackupId,
|
findMySqlByBackupId,
|
||||||
|
findMySqlById,
|
||||||
findPostgresByBackupId,
|
findPostgresByBackupId,
|
||||||
|
findPostgresById,
|
||||||
findServerById,
|
findServerById,
|
||||||
removeBackupById,
|
removeBackupById,
|
||||||
removeScheduleBackup,
|
removeScheduleBackup,
|
||||||
@@ -26,6 +30,17 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { execAsync } from "@dokploy/server/utils/process/execAsync";
|
||||||
|
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
||||||
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
|
import {
|
||||||
|
restoreMariadbBackup,
|
||||||
|
restoreMongoBackup,
|
||||||
|
restoreMySqlBackup,
|
||||||
|
restorePostgresBackup,
|
||||||
|
} from "@dokploy/server/utils/restore";
|
||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
export const backupRouter = createTRPCRouter({
|
export const backupRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -209,27 +224,136 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
listBackupFiles: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
destinationId: z.string(),
|
||||||
|
search: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const destination = await findDestinationById(input.destinationId);
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
|
||||||
|
const lastSlashIndex = input.search.lastIndexOf("/");
|
||||||
|
const baseDir =
|
||||||
|
lastSlashIndex !== -1
|
||||||
|
? input.search.slice(0, lastSlashIndex + 1)
|
||||||
|
: "";
|
||||||
|
const searchTerm =
|
||||||
|
lastSlashIndex !== -1
|
||||||
|
? input.search.slice(lastSlashIndex + 1)
|
||||||
|
: input.search;
|
||||||
|
|
||||||
|
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
|
||||||
|
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(listCommand);
|
||||||
|
const files = stdout.split("\n").filter(Boolean);
|
||||||
|
|
||||||
|
const results = baseDir
|
||||||
|
? files.map((file) => `${baseDir}${file}`)
|
||||||
|
: files;
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
return results.filter((file) =>
|
||||||
|
file.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error listing backup files",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
restoreBackupWithLogs: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
enabled: false,
|
||||||
|
path: "/restore-backup-with-logs",
|
||||||
|
method: "POST",
|
||||||
|
override: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
databaseId: z.string(),
|
||||||
|
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo"]),
|
||||||
|
databaseName: z.string().min(1),
|
||||||
|
backupFile: z.string().min(1),
|
||||||
|
destinationId: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscription(async ({ input }) => {
|
||||||
|
const destination = await findDestinationById(input.destinationId);
|
||||||
|
if (input.databaseType === "postgres") {
|
||||||
|
const postgres = await findPostgresById(input.databaseId);
|
||||||
|
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restorePostgresBackup(
|
||||||
|
postgres,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mysql") {
|
||||||
|
const mysql = await findMySqlById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMySqlBackup(
|
||||||
|
mysql,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mariadb") {
|
||||||
|
const mariadb = await findMariadbById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMariadbBackup(
|
||||||
|
mariadb,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.databaseType === "mongo") {
|
||||||
|
const mongo = await findMongoById(input.databaseId);
|
||||||
|
return observable<string>((emit) => {
|
||||||
|
restoreMongoBackup(
|
||||||
|
mongo,
|
||||||
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// export const getAdminId = async (backupId: string) => {
|
|
||||||
// const backup = await findBackupById(backupId);
|
|
||||||
|
|
||||||
// if (backup.databaseType === "postgres" && backup.postgresId) {
|
|
||||||
// const postgres = await findPostgresById(backup.postgresId);
|
|
||||||
// return postgres.project.adminId;
|
|
||||||
// }
|
|
||||||
// if (backup.databaseType === "mariadb" && backup.mariadbId) {
|
|
||||||
// const mariadb = await findMariadbById(backup.mariadbId);
|
|
||||||
// return mariadb.project.adminId;
|
|
||||||
// }
|
|
||||||
// if (backup.databaseType === "mysql" && backup.mysqlId) {
|
|
||||||
// const mysql = await findMySqlById(backup.mysqlId);
|
|
||||||
// return mysql.project.adminId;
|
|
||||||
// }
|
|
||||||
// if (backup.databaseType === "mongo" && backup.mongoId) {
|
|
||||||
// const mongo = await findMongoById(backup.mongoId);
|
|
||||||
// return mongo.project.adminId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return null;
|
|
||||||
// };
|
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
||||||
import { type DockerNode, IS_CLOUD, docker, execAsync } from "@dokploy/server";
|
import {
|
||||||
|
type DockerNode,
|
||||||
|
IS_CLOUD,
|
||||||
|
execAsync,
|
||||||
|
getRemoteDocker,
|
||||||
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
export const clusterRouter = createTRPCRouter({
|
export const clusterRouter = createTRPCRouter({
|
||||||
getNodes: protectedProcedure.query(async () => {
|
getNodes: protectedProcedure
|
||||||
if (IS_CLOUD) {
|
.input(
|
||||||
return [];
|
z.object({
|
||||||
}
|
serverId: z.string().optional(),
|
||||||
const workers: DockerNode[] = await docker.listNodes();
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return workers;
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
}),
|
const workers: DockerNode[] = await docker.listNodes();
|
||||||
|
|
||||||
|
return workers;
|
||||||
|
}),
|
||||||
removeWorker: protectedProcedure
|
removeWorker: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
nodeId: z.string(),
|
nodeId: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -40,37 +53,51 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
addWorker: protectedProcedure.query(async () => {
|
addWorker: protectedProcedure
|
||||||
if (IS_CLOUD) {
|
.input(
|
||||||
return {
|
z.object({
|
||||||
command: "",
|
serverId: z.string().optional(),
|
||||||
version: "",
|
}),
|
||||||
};
|
)
|
||||||
}
|
.query(async ({ input }) => {
|
||||||
const result = await docker.swarmInspect();
|
if (IS_CLOUD) {
|
||||||
const docker_version = await docker.version();
|
return {
|
||||||
|
command: "",
|
||||||
|
version: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
|
const result = await docker.swarmInspect();
|
||||||
|
const docker_version = await docker.version();
|
||||||
|
|
||||||
return {
|
|
||||||
command: `docker swarm join --token ${
|
|
||||||
result.JoinTokens.Worker
|
|
||||||
} ${await getPublicIpWithFallback()}:2377`,
|
|
||||||
version: docker_version.Version,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
addManager: protectedProcedure.query(async () => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
return {
|
||||||
command: "",
|
command: `docker swarm join --token ${
|
||||||
version: "",
|
result.JoinTokens.Worker
|
||||||
|
} ${await getPublicIpWithFallback()}:2377`,
|
||||||
|
version: docker_version.Version,
|
||||||
};
|
};
|
||||||
}
|
}),
|
||||||
const result = await docker.swarmInspect();
|
addManager: protectedProcedure
|
||||||
const docker_version = await docker.version();
|
.input(
|
||||||
return {
|
z.object({
|
||||||
command: `docker swarm join --token ${
|
serverId: z.string().optional(),
|
||||||
result.JoinTokens.Manager
|
}),
|
||||||
} ${await getPublicIpWithFallback()}:2377`,
|
)
|
||||||
version: docker_version.Version,
|
.query(async ({ input }) => {
|
||||||
};
|
if (IS_CLOUD) {
|
||||||
}),
|
return {
|
||||||
|
command: "",
|
||||||
|
version: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
|
const result = await docker.swarmInspect();
|
||||||
|
const docker_version = await docker.version();
|
||||||
|
return {
|
||||||
|
command: `docker swarm join --token ${
|
||||||
|
result.JoinTokens.Manager
|
||||||
|
} ${await getPublicIpWithFallback()}:2377`,
|
||||||
|
version: docker_version.Version,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -437,13 +437,12 @@ export const composeRouter = createTRPCRouter({
|
|||||||
serverIp = "127.0.0.1";
|
serverIp = "127.0.0.1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
const generate = processTemplate(template.config, {
|
const generate = processTemplate(template.config, {
|
||||||
serverIp: serverIp,
|
serverIp: serverIp,
|
||||||
projectName: project.name,
|
projectName: projectName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
|
||||||
|
|
||||||
const compose = await createComposeByTemplate({
|
const compose = await createComposeByTemplate({
|
||||||
...input,
|
...input,
|
||||||
composeFile: template.dockerCompose,
|
composeFile: template.dockerCompose,
|
||||||
|
|||||||
@@ -97,14 +97,20 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
toggleDashboard: adminProcedure
|
toggleDashboard: adminProcedure
|
||||||
.input(apiEnableDashboard)
|
.input(apiEnableDashboard)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
const ports = (await getTraefikPorts(input.serverId)).filter(
|
||||||
|
(port) =>
|
||||||
|
port.targetPort !== 80 &&
|
||||||
|
port.targetPort !== 443 &&
|
||||||
|
port.targetPort !== 8080,
|
||||||
|
);
|
||||||
await initializeTraefik({
|
await initializeTraefik({
|
||||||
|
additionalPorts: ports,
|
||||||
enableDashboard: input.enableDashboard,
|
enableDashboard: input.enableDashboard,
|
||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cleanUnusedImages: adminProcedure
|
cleanUnusedImages: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -749,7 +755,6 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
targetPort: z.number(),
|
targetPort: z.number(),
|
||||||
publishedPort: z.number(),
|
publishedPort: z.number(),
|
||||||
publishMode: z.enum(["ingress", "host"]).default("host"),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
@@ -782,59 +787,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
getTraefikPorts: adminProcedure
|
getTraefikPorts: adminProcedure
|
||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
|
return await getTraefikPorts(input?.serverId);
|
||||||
|
|
||||||
try {
|
|
||||||
let stdout = "";
|
|
||||||
if (input?.serverId) {
|
|
||||||
const result = await execAsyncRemote(input.serverId, command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
} else if (!IS_CLOUD) {
|
|
||||||
const result = await execAsync(command);
|
|
||||||
stdout = result.stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
const portsMap = JSON.parse(stdout.trim());
|
|
||||||
const additionalPorts: Array<{
|
|
||||||
targetPort: number;
|
|
||||||
publishedPort: number;
|
|
||||||
publishMode: "host" | "ingress";
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
// Convert the Docker container port format to our expected format
|
|
||||||
for (const [containerPort, bindings] of Object.entries(portsMap)) {
|
|
||||||
if (!bindings) continue;
|
|
||||||
|
|
||||||
const [port = ""] = containerPort.split("/");
|
|
||||||
if (!port) continue;
|
|
||||||
|
|
||||||
const targetPortNum = Number.parseInt(port, 10);
|
|
||||||
if (Number.isNaN(targetPortNum)) continue;
|
|
||||||
|
|
||||||
// Skip default ports
|
|
||||||
if ([80, 443, 8080].includes(targetPortNum)) continue;
|
|
||||||
|
|
||||||
for (const binding of bindings as Array<{ HostPort: string }>) {
|
|
||||||
if (!binding.HostPort) continue;
|
|
||||||
const publishedPort = Number.parseInt(binding.HostPort, 10);
|
|
||||||
if (Number.isNaN(publishedPort)) continue;
|
|
||||||
|
|
||||||
additionalPorts.push({
|
|
||||||
targetPort: targetPortNum,
|
|
||||||
publishedPort,
|
|
||||||
publishMode: "host", // Docker standalone uses host mode by default
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return additionalPorts;
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: "Failed to get Traefik ports",
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
updateLogCleanup: adminProcedure
|
updateLogCleanup: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -853,3 +806,56 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
return getLogCleanupStatus();
|
return getLogCleanupStatus();
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getTraefikPorts = async (serverId?: string) => {
|
||||||
|
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
|
||||||
|
try {
|
||||||
|
let stdout = "";
|
||||||
|
if (serverId) {
|
||||||
|
const result = await execAsyncRemote(serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
} else if (!IS_CLOUD) {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
const portsMap = JSON.parse(stdout.trim());
|
||||||
|
const additionalPorts: Array<{
|
||||||
|
targetPort: number;
|
||||||
|
publishedPort: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Convert the Docker container port format to our expected format
|
||||||
|
for (const [containerPort, bindings] of Object.entries(portsMap)) {
|
||||||
|
if (!bindings) continue;
|
||||||
|
|
||||||
|
const [port = ""] = containerPort.split("/");
|
||||||
|
if (!port) continue;
|
||||||
|
|
||||||
|
const targetPortNum = Number.parseInt(port, 10);
|
||||||
|
if (Number.isNaN(targetPortNum)) continue;
|
||||||
|
|
||||||
|
// Skip default ports
|
||||||
|
if ([80, 443].includes(targetPortNum)) continue;
|
||||||
|
|
||||||
|
for (const binding of bindings as Array<{ HostPort: string }>) {
|
||||||
|
if (!binding.HostPort) continue;
|
||||||
|
const publishedPort = Number.parseInt(binding.HostPort, 10);
|
||||||
|
if (Number.isNaN(publishedPort)) continue;
|
||||||
|
|
||||||
|
additionalPorts.push({
|
||||||
|
targetPort: targetPortNum,
|
||||||
|
publishedPort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return additionalPorts;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Failed to get Traefik ports",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export const setupDeploymentLogsWebSocketServer = (
|
|||||||
}
|
}
|
||||||
stream
|
stream
|
||||||
.on("close", () => {
|
.on("close", () => {
|
||||||
console.log("Connection closed ✅");
|
|
||||||
client.end();
|
client.end();
|
||||||
ws.close();
|
ws.close();
|
||||||
})
|
})
|
||||||
@@ -86,7 +85,6 @@ export const setupDeploymentLogsWebSocketServer = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
console.log("Connection closed ✅, From WS");
|
|
||||||
client.end();
|
client.end();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const config = {
|
|||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
|
sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
|
||||||
},
|
},
|
||||||
|
screens: {
|
||||||
|
"3xl": "120rem",
|
||||||
|
},
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
"2xl": "40rem",
|
"2xl": "40rem",
|
||||||
"8xl": "85rem",
|
"8xl": "85rem",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"@oslojs/encoding":"1.1.0",
|
"@oslojs/encoding":"1.1.0",
|
||||||
"@oslojs/crypto":"1.0.1",
|
"@oslojs/crypto":"1.0.1",
|
||||||
"drizzle-dbml-generator":"0.10.0",
|
"drizzle-dbml-generator":"0.10.0",
|
||||||
"better-auth":"1.2.0",
|
"better-auth":"1.2.4",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@lucia-auth/adapter-drizzle": "1.0.7",
|
"@lucia-auth/adapter-drizzle": "1.0.7",
|
||||||
"@octokit/auth-app": "^6.0.4",
|
"@octokit/auth-app": "^6.0.4",
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const applications = pgTable("application", {
|
|||||||
command: text("command"),
|
command: text("command"),
|
||||||
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
|
||||||
sourceType: sourceType("sourceType").notNull().default("github"),
|
sourceType: sourceType("sourceType").notNull().default("github"),
|
||||||
|
cleanCache: boolean("cleanCache").default(false),
|
||||||
// Github
|
// Github
|
||||||
repository: text("repository"),
|
repository: text("repository"),
|
||||||
owner: text("owner"),
|
owner: text("owner"),
|
||||||
@@ -408,6 +409,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewPath: z.string().optional(),
|
previewPath: z.string().optional(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
|
cleanCache: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateApplication = createSchema.pick({
|
export const apiCreateApplication = createSchema.pick({
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ table application {
|
|||||||
command text
|
command text
|
||||||
refreshToken text
|
refreshToken text
|
||||||
sourceType sourceType [not null, default: 'github']
|
sourceType sourceType [not null, default: 'github']
|
||||||
|
cleanCache boolean [default: false]
|
||||||
repository text
|
repository text
|
||||||
owner text
|
owner text
|
||||||
branch text
|
branch text
|
||||||
|
|||||||
@@ -28,6 +28,26 @@ const { handler, api } = betterAuth({
|
|||||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(!IS_CLOUD && {
|
||||||
|
async trustedOrigins() {
|
||||||
|
const admin = await db.query.member.findFirst({
|
||||||
|
where: eq(schema.member.role, "owner"),
|
||||||
|
with: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (admin) {
|
||||||
|
return [
|
||||||
|
...(admin.user.serverIp
|
||||||
|
? [`http://${admin.user.serverIp}:3000`]
|
||||||
|
: []),
|
||||||
|
...(admin.user.host ? [`https://${admin.user.host}`] : []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}),
|
||||||
emailVerification: {
|
emailVerification: {
|
||||||
sendOnSignUp: true,
|
sendOnSignUp: true,
|
||||||
autoSignInAfterVerification: true,
|
autoSignInAfterVerification: true,
|
||||||
@@ -117,6 +137,10 @@ const { handler, api } = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
expiresIn: 60 * 60 * 24 * 3,
|
||||||
|
updateAge: 60 * 60 * 24,
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
modelName: "users_temp",
|
modelName: "users_temp",
|
||||||
additionalFields: {
|
additionalFields: {
|
||||||
|
|||||||
@@ -182,12 +182,6 @@ export const deployApplication = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const admin = await findUserById(application.project.userId);
|
|
||||||
|
|
||||||
// if (admin.cleanupCacheApplications) {
|
|
||||||
// await cleanupFullDocker(application?.serverId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
await cloneGithubRepository({
|
||||||
...application,
|
...application,
|
||||||
@@ -257,11 +251,6 @@ export const rebuildApplication = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const admin = await findUserById(application.project.userId);
|
|
||||||
|
|
||||||
// if (admin.cleanupCacheApplications) {
|
|
||||||
// await cleanupFullDocker(application?.serverId);
|
|
||||||
// }
|
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
} else if (application.sourceType === "gitlab") {
|
} else if (application.sourceType === "gitlab") {
|
||||||
@@ -306,11 +295,6 @@ export const deployRemoteApplication = async ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
// const admin = await findUserById(application.project.userId);
|
|
||||||
|
|
||||||
// if (admin.cleanupCacheApplications) {
|
|
||||||
// await cleanupFullDocker(application?.serverId);
|
|
||||||
// }
|
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
command += await getGithubCloneCommand({
|
command += await getGithubCloneCommand({
|
||||||
@@ -451,12 +435,6 @@ export const deployPreviewApplication = async ({
|
|||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
|
|
||||||
// const admin = await findUserById(application.project.userId);
|
|
||||||
|
|
||||||
// if (admin.cleanupCacheOnPreviews) {
|
|
||||||
// await cleanupFullDocker(application?.serverId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
await cloneGithubRepository({
|
||||||
...application,
|
...application,
|
||||||
@@ -565,11 +543,6 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
|
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
// const admin = await findUserById(application.project.userId);
|
|
||||||
|
|
||||||
// if (admin.cleanupCacheOnPreviews) {
|
|
||||||
// await cleanupFullDocker(application?.serverId);
|
|
||||||
// }
|
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
command += await getGithubCloneCommand({
|
command += await getGithubCloneCommand({
|
||||||
@@ -634,11 +607,6 @@ export const rebuildRemoteApplication = async ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
// const admin = await findUserById(application.project.userId);
|
|
||||||
|
|
||||||
// if (admin.cleanupCacheApplications) {
|
|
||||||
// await cleanupFullDocker(application?.serverId);
|
|
||||||
// }
|
|
||||||
if (application.sourceType !== "docker") {
|
if (application.sourceType !== "docker") {
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
command += getBuildCommand(application, deployment.logPath);
|
command += getBuildCommand(application, deployment.logPath);
|
||||||
|
|||||||
@@ -216,10 +216,6 @@ export const deployCompose = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const admin = await findUserById(compose.project.userId);
|
|
||||||
// if (admin.cleanupCacheOnCompose) {
|
|
||||||
// await cleanupFullDocker(compose?.serverId);
|
|
||||||
// }
|
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
await cloneGithubRepository({
|
||||||
...compose,
|
...compose,
|
||||||
@@ -285,15 +281,10 @@ export const rebuildCompose = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const admin = await findUserById(compose.project.userId);
|
if (compose.sourceType === "raw") {
|
||||||
// if (admin.cleanupCacheOnCompose) {
|
await createComposeFile(compose, deployment.logPath);
|
||||||
// await cleanupFullDocker(compose?.serverId);
|
|
||||||
// }
|
|
||||||
if (compose.serverId) {
|
|
||||||
await getBuildComposeCommand(compose, deployment.logPath);
|
|
||||||
} else {
|
|
||||||
await buildCompose(compose, deployment.logPath);
|
|
||||||
}
|
}
|
||||||
|
await buildCompose(compose, deployment.logPath);
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updateCompose(composeId, {
|
await updateCompose(composeId, {
|
||||||
@@ -331,10 +322,6 @@ export const deployRemoteCompose = async ({
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
// const admin = await findUserById(compose.project.userId);
|
|
||||||
// if (admin.cleanupCacheOnCompose) {
|
|
||||||
// await cleanupFullDocker(compose?.serverId);
|
|
||||||
// }
|
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
|
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
@@ -429,10 +416,10 @@ export const rebuildRemoteCompose = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const admin = await findUserById(compose.project.userId);
|
if (compose.sourceType === "raw") {
|
||||||
// if (admin.cleanupCacheOnCompose) {
|
const command = getCreateComposeFileCommand(compose, deployment.logPath);
|
||||||
// await cleanupFullDocker(compose?.serverId);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
// }
|
}
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await getBuildComposeCommand(compose, deployment.logPath);
|
await getBuildComposeCommand(compose, deployment.logPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,26 +136,24 @@ export const getContainersByAppNameMatch = async (
|
|||||||
result = stdout.trim().split("\n");
|
result = stdout.trim().split("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = result
|
const containers = result.map((line) => {
|
||||||
.map((line) => {
|
const parts = line.split(" | ");
|
||||||
const parts = line.split(" | ");
|
const containerId = parts[0]
|
||||||
const containerId = parts[0]
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
: "No container id";
|
||||||
: "No container id";
|
const name = parts[1]
|
||||||
const name = parts[1]
|
? parts[1].replace("Name: ", "").trim()
|
||||||
? parts[1].replace("Name: ", "").trim()
|
: "No container name";
|
||||||
: "No container name";
|
|
||||||
|
|
||||||
const state = parts[2]
|
const state = parts[2]
|
||||||
? parts[2].replace("State: ", "").trim()
|
? parts[2].replace("State: ", "").trim()
|
||||||
: "No state";
|
: "No state";
|
||||||
return {
|
return {
|
||||||
containerId,
|
containerId,
|
||||||
name,
|
name,
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
@@ -192,30 +190,28 @@ export const getStackContainersByAppName = async (
|
|||||||
result = stdout.trim().split("\n");
|
result = stdout.trim().split("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = result
|
const containers = result.map((line) => {
|
||||||
.map((line) => {
|
const parts = line.split(" | ");
|
||||||
const parts = line.split(" | ");
|
const containerId = parts[0]
|
||||||
const containerId = parts[0]
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
: "No container id";
|
||||||
: "No container id";
|
const name = parts[1]
|
||||||
const name = parts[1]
|
? parts[1].replace("Name: ", "").trim()
|
||||||
? parts[1].replace("Name: ", "").trim()
|
: "No container name";
|
||||||
: "No container name";
|
|
||||||
|
|
||||||
const state = parts[2]
|
const state = parts[2]
|
||||||
? parts[2].replace("State: ", "").trim().toLowerCase()
|
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||||
: "No state";
|
: "No state";
|
||||||
const node = parts[3]
|
const node = parts[3]
|
||||||
? parts[3].replace("Node: ", "").trim()
|
? parts[3].replace("Node: ", "").trim()
|
||||||
: "No specific node";
|
: "No specific node";
|
||||||
return {
|
return {
|
||||||
containerId,
|
containerId,
|
||||||
name,
|
name,
|
||||||
state,
|
state,
|
||||||
node,
|
node,
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
@@ -253,31 +249,29 @@ export const getServiceContainersByAppName = async (
|
|||||||
result = stdout.trim().split("\n");
|
result = stdout.trim().split("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const containers = result
|
const containers = result.map((line) => {
|
||||||
.map((line) => {
|
const parts = line.split(" | ");
|
||||||
const parts = line.split(" | ");
|
const containerId = parts[0]
|
||||||
const containerId = parts[0]
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
: "No container id";
|
||||||
: "No container id";
|
const name = parts[1]
|
||||||
const name = parts[1]
|
? parts[1].replace("Name: ", "").trim()
|
||||||
? parts[1].replace("Name: ", "").trim()
|
: "No container name";
|
||||||
: "No container name";
|
|
||||||
|
|
||||||
const state = parts[2]
|
const state = parts[2]
|
||||||
? parts[2].replace("State: ", "").trim().toLowerCase()
|
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||||
: "No state";
|
: "No state";
|
||||||
|
|
||||||
const node = parts[3]
|
const node = parts[3]
|
||||||
? parts[3].replace("Node: ", "").trim()
|
? parts[3].replace("Node: ", "").trim()
|
||||||
: "No specific node";
|
: "No specific node";
|
||||||
return {
|
return {
|
||||||
containerId,
|
containerId,
|
||||||
name,
|
name,
|
||||||
state,
|
state,
|
||||||
node,
|
node,
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
@@ -318,25 +312,23 @@ export const getContainersByAppLabel = async (
|
|||||||
|
|
||||||
const lines = stdout.trim().split("\n");
|
const lines = stdout.trim().split("\n");
|
||||||
|
|
||||||
const containers = lines
|
const containers = lines.map((line) => {
|
||||||
.map((line) => {
|
const parts = line.split(" | ");
|
||||||
const parts = line.split(" | ");
|
const containerId = parts[0]
|
||||||
const containerId = parts[0]
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
: "No container id";
|
||||||
: "No container id";
|
const name = parts[1]
|
||||||
const name = parts[1]
|
? parts[1].replace("Name: ", "").trim()
|
||||||
? parts[1].replace("Name: ", "").trim()
|
: "No container name";
|
||||||
: "No container name";
|
const state = parts[2]
|
||||||
const state = parts[2]
|
? parts[2].replace("State: ", "").trim()
|
||||||
? parts[2].replace("State: ", "").trim()
|
: "No state";
|
||||||
: "No state";
|
return {
|
||||||
return {
|
containerId,
|
||||||
containerId,
|
name,
|
||||||
name,
|
state,
|
||||||
state,
|
};
|
||||||
};
|
});
|
||||||
})
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
return containers || [];
|
return containers || [];
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ interface TraefikOptions {
|
|||||||
additionalPorts?: {
|
additionalPorts?: {
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
publishedPort: number;
|
publishedPort: number;
|
||||||
publishMode?: "ingress" | "host";
|
|
||||||
}[];
|
}[];
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,14 @@ export const buildCustomDocker = async (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
writeStream: WriteStream,
|
writeStream: WriteStream,
|
||||||
) => {
|
) => {
|
||||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
const {
|
||||||
application;
|
appName,
|
||||||
|
env,
|
||||||
|
publishDirectory,
|
||||||
|
buildArgs,
|
||||||
|
dockerBuildStage,
|
||||||
|
cleanCache,
|
||||||
|
} = application;
|
||||||
const dockerFilePath = getBuildAppDirectory(application);
|
const dockerFilePath = getBuildAppDirectory(application);
|
||||||
try {
|
try {
|
||||||
const image = `${appName}`;
|
const image = `${appName}`;
|
||||||
@@ -29,6 +35,10 @@ export const buildCustomDocker = async (
|
|||||||
|
|
||||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
commandArgs.push("--no-cache");
|
||||||
|
}
|
||||||
|
|
||||||
if (dockerBuildStage) {
|
if (dockerBuildStage) {
|
||||||
commandArgs.push("--target", dockerBuildStage);
|
commandArgs.push("--target", dockerBuildStage);
|
||||||
}
|
}
|
||||||
@@ -65,8 +75,14 @@ export const getDockerCommand = (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
) => {
|
) => {
|
||||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
const {
|
||||||
application;
|
appName,
|
||||||
|
env,
|
||||||
|
publishDirectory,
|
||||||
|
buildArgs,
|
||||||
|
dockerBuildStage,
|
||||||
|
cleanCache,
|
||||||
|
} = application;
|
||||||
const dockerFilePath = getBuildAppDirectory(application);
|
const dockerFilePath = getBuildAppDirectory(application);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -88,6 +104,10 @@ export const getDockerCommand = (
|
|||||||
commandArgs.push("--target", dockerBuildStage);
|
commandArgs.push("--target", dockerBuildStage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
commandArgs.push("--no-cache");
|
||||||
|
}
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
commandArgs.push("--build-arg", arg);
|
commandArgs.push("--build-arg", arg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const buildHeroku = async (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
writeStream: WriteStream,
|
writeStream: WriteStream,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
env,
|
env,
|
||||||
@@ -29,6 +29,10 @@ export const buildHeroku = async (
|
|||||||
args.push("--env", env);
|
args.push("--env", env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
args.push("--clear-cache");
|
||||||
|
}
|
||||||
|
|
||||||
await spawnAsync("pack", args, (data) => {
|
await spawnAsync("pack", args, (data) => {
|
||||||
if (writeStream.writable) {
|
if (writeStream.writable) {
|
||||||
writeStream.write(data);
|
writeStream.write(data);
|
||||||
@@ -44,7 +48,7 @@ export const getHerokuCommand = (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
@@ -61,6 +65,10 @@ export const getHerokuCommand = (
|
|||||||
`heroku/builder:${application.herokuVersion || "24"}`,
|
`heroku/builder:${application.herokuVersion || "24"}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
args.push("--clear-cache");
|
||||||
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", `'${env}'`);
|
args.push("--env", `'${env}'`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const buildNixpacks = async (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
writeStream: WriteStream,
|
writeStream: WriteStream,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName, publishDirectory } = application;
|
const { env, appName, publishDirectory, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||||
@@ -32,6 +32,10 @@ export const buildNixpacks = async (
|
|||||||
try {
|
try {
|
||||||
const args = ["build", buildAppDirectory, "--name", appName];
|
const args = ["build", buildAppDirectory, "--name", appName];
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
args.push("--no-cache");
|
||||||
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", env);
|
args.push("--env", env);
|
||||||
}
|
}
|
||||||
@@ -91,7 +95,7 @@ export const getNixpacksCommand = (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName, publishDirectory } = application;
|
const { env, appName, publishDirectory, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||||
@@ -102,6 +106,10 @@ export const getNixpacksCommand = (
|
|||||||
|
|
||||||
const args = ["build", buildAppDirectory, "--name", appName];
|
const args = ["build", buildAppDirectory, "--name", appName];
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
args.push("--no-cache");
|
||||||
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", `'${env}'`);
|
args.push("--env", `'${env}'`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const buildPaketo = async (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
writeStream: WriteStream,
|
writeStream: WriteStream,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
env,
|
env,
|
||||||
@@ -24,6 +24,10 @@ export const buildPaketo = async (
|
|||||||
"paketobuildpacks/builder-jammy-full",
|
"paketobuildpacks/builder-jammy-full",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
args.push("--clear-cache");
|
||||||
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", env);
|
args.push("--env", env);
|
||||||
}
|
}
|
||||||
@@ -43,7 +47,7 @@ export const getPaketoCommand = (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
|
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
@@ -60,6 +64,10 @@ export const getPaketoCommand = (
|
|||||||
"paketobuildpacks/builder-jammy-full",
|
"paketobuildpacks/builder-jammy-full",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (cleanCache) {
|
||||||
|
args.push("--clear-cache");
|
||||||
|
}
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", `'${env}'`);
|
args.push("--env", `'${env}'`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,22 @@ import { prepareEnvironmentVariables } from "../docker/utils";
|
|||||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
import { spawnAsync } from "../process/spawnAsync";
|
||||||
import { execAsync } from "../process/execAsync";
|
import { execAsync } from "../process/execAsync";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
|
const calculateSecretsHash = (envVariables: string[]): string => {
|
||||||
|
const hash = createHash("sha256");
|
||||||
|
for (const env of envVariables.sort()) {
|
||||||
|
hash.update(env);
|
||||||
|
}
|
||||||
|
return hash.digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
export const buildRailpack = async (
|
export const buildRailpack = async (
|
||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
writeStream: WriteStream,
|
writeStream: WriteStream,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
env,
|
env,
|
||||||
@@ -17,32 +27,80 @@ export const buildRailpack = async (
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure buildkit container is running, create if it doesn't exist
|
|
||||||
await execAsync(
|
await execAsync(
|
||||||
"docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit",
|
"docker buildx create --use --name builder-containerd --driver docker-container || true",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build the application using railpack
|
await execAsync("docker buildx use builder-containerd");
|
||||||
const args = ["build", buildAppDirectory, "--name", appName];
|
|
||||||
|
|
||||||
// Add environment variables
|
// First prepare the build plan and info
|
||||||
|
const prepareArgs = [
|
||||||
|
"prepare",
|
||||||
|
buildAppDirectory,
|
||||||
|
"--plan-out",
|
||||||
|
`${buildAppDirectory}/railpack-plan.json`,
|
||||||
|
"--info-out",
|
||||||
|
`${buildAppDirectory}/railpack-info.json`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add environment variables to prepare command
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", env);
|
prepareArgs.push("--env", env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run prepare command
|
||||||
|
await spawnAsync("railpack", prepareArgs, (data) => {
|
||||||
|
if (writeStream.writable) {
|
||||||
|
writeStream.write(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate secrets hash for layer invalidation
|
||||||
|
const secretsHash = calculateSecretsHash(envVariables);
|
||||||
|
|
||||||
|
// Build with BuildKit using the Railpack frontend
|
||||||
|
const cacheKey = cleanCache ? nanoid(10) : undefined;
|
||||||
|
const buildArgs = [
|
||||||
|
"buildx",
|
||||||
|
"build",
|
||||||
|
...(cacheKey
|
||||||
|
? [
|
||||||
|
"--build-arg",
|
||||||
|
`secrets-hash=${secretsHash}`,
|
||||||
|
"--build-arg",
|
||||||
|
`cache-key=${cacheKey}`,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
"--build-arg",
|
||||||
|
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
|
||||||
|
"-f",
|
||||||
|
`${buildAppDirectory}/railpack-plan.json`,
|
||||||
|
"--output",
|
||||||
|
`type=docker,name=${appName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add secrets properly formatted
|
||||||
|
const env: { [key: string]: string } = {};
|
||||||
|
for (const envVar of envVariables) {
|
||||||
|
const [key, value] = envVar.split("=");
|
||||||
|
if (key && value) {
|
||||||
|
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||||
|
env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildArgs.push(buildAppDirectory);
|
||||||
|
|
||||||
await spawnAsync(
|
await spawnAsync(
|
||||||
"railpack",
|
"docker",
|
||||||
args,
|
buildArgs,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (writeStream.writable) {
|
if (writeStream.writable) {
|
||||||
writeStream.write(data);
|
writeStream.write(data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
env: {
|
env: { ...process.env, ...env },
|
||||||
...process.env,
|
|
||||||
BUILDKIT_HOST: "docker-container://buildkit",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,32 +114,84 @@ export const getRailpackCommand = (
|
|||||||
application: ApplicationNested,
|
application: ApplicationNested,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
) => {
|
) => {
|
||||||
const { env, appName } = application;
|
const { env, appName, cleanCache } = application;
|
||||||
const buildAppDirectory = getBuildAppDirectory(application);
|
const buildAppDirectory = getBuildAppDirectory(application);
|
||||||
const envVariables = prepareEnvironmentVariables(
|
const envVariables = prepareEnvironmentVariables(
|
||||||
env,
|
env,
|
||||||
application.project.env,
|
application.project.env,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build the application using railpack
|
// Prepare command
|
||||||
const args = ["build", buildAppDirectory, "--name", appName];
|
const prepareArgs = [
|
||||||
|
"prepare",
|
||||||
|
buildAppDirectory,
|
||||||
|
"--plan-out",
|
||||||
|
`${buildAppDirectory}/railpack-plan.json`,
|
||||||
|
"--info-out",
|
||||||
|
`${buildAppDirectory}/railpack-info.json`,
|
||||||
|
];
|
||||||
|
|
||||||
// Add environment variables
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
args.push("--env", env);
|
prepareArgs.push("--env", env);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = `railpack ${args.join(" ")}`;
|
// Calculate secrets hash for layer invalidation
|
||||||
|
const secretsHash = calculateSecretsHash(envVariables);
|
||||||
|
|
||||||
|
const cacheKey = cleanCache ? nanoid(10) : undefined;
|
||||||
|
// Build command
|
||||||
|
const buildArgs = [
|
||||||
|
"buildx",
|
||||||
|
"build",
|
||||||
|
...(cacheKey
|
||||||
|
? [
|
||||||
|
"--build-arg",
|
||||||
|
`secrets-hash=${secretsHash}`,
|
||||||
|
"--build-arg",
|
||||||
|
`cache-key=${cacheKey}`,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
"--build-arg",
|
||||||
|
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
|
||||||
|
"-f",
|
||||||
|
`${buildAppDirectory}/railpack-plan.json`,
|
||||||
|
"--output",
|
||||||
|
`type=docker,name=${appName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add secrets properly formatted
|
||||||
|
const exportEnvs = [];
|
||||||
|
for (const envVar of envVariables) {
|
||||||
|
const [key, value] = envVar.split("=");
|
||||||
|
if (key && value) {
|
||||||
|
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||||
|
exportEnvs.push(`export ${key}=${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildArgs.push(buildAppDirectory);
|
||||||
|
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
echo "Building with Railpack..." >> "${logPath}";
|
# Ensure we have a builder with containerd
|
||||||
docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit;
|
docker buildx create --use --name builder-containerd --driver docker-container || true
|
||||||
export BUILDKIT_HOST=docker-container://buildkit;
|
docker buildx use builder-containerd
|
||||||
${command} >> ${logPath} 2>> ${logPath} || {
|
|
||||||
echo "❌ Railpack build failed" >> ${logPath};
|
echo "Preparing Railpack build plan..." >> "${logPath}";
|
||||||
exit 1;
|
railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||||
}
|
echo "❌ Railpack prepare failed" >> ${logPath};
|
||||||
echo "✅ Railpack build completed." >> ${logPath};
|
exit 1;
|
||||||
`;
|
}
|
||||||
|
echo "✅ Railpack prepare completed." >> ${logPath};
|
||||||
|
|
||||||
|
echo "Building with Railpack frontend..." >> "${logPath}";
|
||||||
|
# Export environment variables for secrets
|
||||||
|
${exportEnvs.join("\n")}
|
||||||
|
docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||||
|
echo "❌ Railpack build failed" >> ${logPath};
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
echo "✅ Railpack build completed." >> ${logPath};
|
||||||
|
`;
|
||||||
|
|
||||||
return bashCommand;
|
return bashCommand;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -238,9 +238,9 @@ export const addDomainToCompose = async (
|
|||||||
|
|
||||||
if (Array.isArray(labels)) {
|
if (Array.isArray(labels)) {
|
||||||
if (!labels.includes("traefik.enable=true")) {
|
if (!labels.includes("traefik.enable=true")) {
|
||||||
labels.push("traefik.enable=true");
|
labels.unshift("traefik.enable=true");
|
||||||
}
|
}
|
||||||
labels.push(...httpLabels);
|
labels.unshift(...httpLabels);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!compose.isolatedDeployment) {
|
if (!compose.isolatedDeployment) {
|
||||||
|
|||||||
4
packages/server/src/utils/restore/index.ts
Normal file
4
packages/server/src/utils/restore/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { restorePostgresBackup } from "./postgres";
|
||||||
|
export { restoreMySqlBackup } from "./mysql";
|
||||||
|
export { restoreMariadbBackup } from "./mariadb";
|
||||||
|
export { restoreMongoBackup } from "./mongo";
|
||||||
56
packages/server/src/utils/restore/mariadb.ts
Normal file
56
packages/server/src/utils/restore/mariadb.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||||
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
|
import {
|
||||||
|
getRemoteServiceContainer,
|
||||||
|
getServiceContainer,
|
||||||
|
} from "../docker/utils";
|
||||||
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { getS3Credentials } from "../backups/utils";
|
||||||
|
|
||||||
|
export const restoreMariadbBackup = async (
|
||||||
|
mariadb: Mariadb,
|
||||||
|
destination: Destination,
|
||||||
|
database: string,
|
||||||
|
backupFile: string,
|
||||||
|
emit: (log: string) => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { appName, databasePassword, databaseUser, serverId } = mariadb;
|
||||||
|
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
const backupPath = `${bucketPath}/${backupFile}`;
|
||||||
|
|
||||||
|
const { Id: containerName } = serverId
|
||||||
|
? await getRemoteServiceContainer(serverId, appName)
|
||||||
|
: await getServiceContainer(appName);
|
||||||
|
|
||||||
|
const restoreCommand = `
|
||||||
|
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerName} mariadb -u ${databaseUser} -p${databasePassword} ${database}
|
||||||
|
`;
|
||||||
|
|
||||||
|
emit("Starting restore...");
|
||||||
|
|
||||||
|
emit(`Executing command: ${restoreCommand}`);
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, restoreCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(restoreCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("Restore completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
emit(
|
||||||
|
`Error: ${
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error restoring mariadb backup"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
error instanceof Error ? error.message : "Error restoring mariadb backup",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
64
packages/server/src/utils/restore/mongo.ts
Normal file
64
packages/server/src/utils/restore/mongo.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||||
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
|
import {
|
||||||
|
getRemoteServiceContainer,
|
||||||
|
getServiceContainer,
|
||||||
|
} from "../docker/utils";
|
||||||
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { getS3Credentials } from "../backups/utils";
|
||||||
|
|
||||||
|
export const restoreMongoBackup = async (
|
||||||
|
mongo: Mongo,
|
||||||
|
destination: Destination,
|
||||||
|
database: string,
|
||||||
|
backupFile: string,
|
||||||
|
emit: (log: string) => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { appName, databasePassword, databaseUser, serverId } = mongo;
|
||||||
|
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
const backupPath = `${bucketPath}/${backupFile}`;
|
||||||
|
|
||||||
|
const { Id: containerName } = serverId
|
||||||
|
? await getRemoteServiceContainer(serverId, appName)
|
||||||
|
: await getServiceContainer(appName);
|
||||||
|
|
||||||
|
// For MongoDB, we need to first download the backup file since mongorestore expects a directory
|
||||||
|
const tempDir = "/tmp/dokploy-restore";
|
||||||
|
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
|
||||||
|
const decompressedName = fileName.replace(".gz", "");
|
||||||
|
|
||||||
|
const downloadCommand = `\
|
||||||
|
rm -rf ${tempDir} && \
|
||||||
|
mkdir -p ${tempDir} && \
|
||||||
|
rclone copy ${rcloneFlags.join(" ")} "${backupPath}" ${tempDir} && \
|
||||||
|
cd ${tempDir} && \
|
||||||
|
gunzip -f "${fileName}" && \
|
||||||
|
docker exec -i ${containerName} mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive < "${decompressedName}" && \
|
||||||
|
rm -rf ${tempDir}`;
|
||||||
|
|
||||||
|
emit("Starting restore...");
|
||||||
|
|
||||||
|
emit(`Executing command: ${downloadCommand}`);
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, downloadCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(downloadCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("Restore completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
emit(
|
||||||
|
`Error: ${
|
||||||
|
error instanceof Error ? error.message : "Error restoring mongo backup"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
error instanceof Error ? error.message : "Error restoring mongo backup",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
54
packages/server/src/utils/restore/mysql.ts
Normal file
54
packages/server/src/utils/restore/mysql.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { MySql } from "@dokploy/server/services/mysql";
|
||||||
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
|
import {
|
||||||
|
getRemoteServiceContainer,
|
||||||
|
getServiceContainer,
|
||||||
|
} from "../docker/utils";
|
||||||
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { getS3Credentials } from "../backups/utils";
|
||||||
|
|
||||||
|
export const restoreMySqlBackup = async (
|
||||||
|
mysql: MySql,
|
||||||
|
destination: Destination,
|
||||||
|
database: string,
|
||||||
|
backupFile: string,
|
||||||
|
emit: (log: string) => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { appName, databaseRootPassword, serverId } = mysql;
|
||||||
|
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
const backupPath = `${bucketPath}/${backupFile}`;
|
||||||
|
|
||||||
|
const { Id: containerName } = serverId
|
||||||
|
? await getRemoteServiceContainer(serverId, appName)
|
||||||
|
: await getServiceContainer(appName);
|
||||||
|
|
||||||
|
const restoreCommand = `
|
||||||
|
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerName} mysql -u root -p${databaseRootPassword} ${database}
|
||||||
|
`;
|
||||||
|
|
||||||
|
emit("Starting restore...");
|
||||||
|
|
||||||
|
emit(`Executing command: ${restoreCommand}`);
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
await execAsyncRemote(serverId, restoreCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(restoreCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("Restore completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
emit(
|
||||||
|
`Error: ${
|
||||||
|
error instanceof Error ? error.message : "Error restoring mysql backup"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
error instanceof Error ? error.message : "Error restoring mysql backup",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
60
packages/server/src/utils/restore/postgres.ts
Normal file
60
packages/server/src/utils/restore/postgres.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||||
|
import type { Destination } from "@dokploy/server/services/destination";
|
||||||
|
import {
|
||||||
|
getRemoteServiceContainer,
|
||||||
|
getServiceContainer,
|
||||||
|
} from "../docker/utils";
|
||||||
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { getS3Credentials } from "../backups/utils";
|
||||||
|
|
||||||
|
export const restorePostgresBackup = async (
|
||||||
|
postgres: Postgres,
|
||||||
|
destination: Destination,
|
||||||
|
database: string,
|
||||||
|
backupFile: string,
|
||||||
|
emit: (log: string) => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { appName, databaseUser, serverId } = postgres;
|
||||||
|
|
||||||
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
|
||||||
|
const backupPath = `${bucketPath}/${backupFile}`;
|
||||||
|
|
||||||
|
const { Id: containerName } = serverId
|
||||||
|
? await getRemoteServiceContainer(serverId, appName)
|
||||||
|
: await getServiceContainer(appName);
|
||||||
|
|
||||||
|
emit("Starting restore...");
|
||||||
|
emit(`Backup path: ${backupPath}`);
|
||||||
|
|
||||||
|
const command = `\
|
||||||
|
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerName} pg_restore -U ${databaseUser} -d ${database} --clean --if-exists`;
|
||||||
|
|
||||||
|
emit(`Executing command: ${command}`);
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||||
|
emit(stdout);
|
||||||
|
emit(stderr);
|
||||||
|
} else {
|
||||||
|
const { stdout, stderr } = await execAsync(command);
|
||||||
|
console.log("stdout", stdout);
|
||||||
|
console.log("stderr", stderr);
|
||||||
|
emit(stdout);
|
||||||
|
emit(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("Restore completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
emit(
|
||||||
|
`Error: ${
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error restoring postgres backup"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
46
pnpm-lock.yaml
generated
46
pnpm-lock.yaml
generated
@@ -269,8 +269,8 @@ importers:
|
|||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1(encoding@0.1.13)
|
version: 5.1.1(encoding@0.1.13)
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.2.0
|
specifier: 1.2.4
|
||||||
version: 1.2.0(typescript@5.5.3)
|
version: 1.2.4(typescript@5.5.3)
|
||||||
bl:
|
bl:
|
||||||
specifier: 6.0.11
|
specifier: 6.0.11
|
||||||
version: 6.0.11
|
version: 6.0.11
|
||||||
@@ -303,10 +303,10 @@ importers:
|
|||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.39.1
|
specifier: ^0.39.1
|
||||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||||
drizzle-zod:
|
drizzle-zod:
|
||||||
specifier: 0.5.1
|
specifier: 0.5.1
|
||||||
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
||||||
fancy-ansi:
|
fancy-ansi:
|
||||||
specifier: ^0.1.3
|
specifier: ^0.1.3
|
||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
@@ -547,7 +547,7 @@ importers:
|
|||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.39.1
|
specifier: ^0.39.1
|
||||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||||
hono:
|
hono:
|
||||||
specifier: ^4.5.8
|
specifier: ^4.5.8
|
||||||
version: 4.5.8
|
version: 4.5.8
|
||||||
@@ -646,8 +646,8 @@ importers:
|
|||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1(encoding@0.1.13)
|
version: 5.1.1(encoding@0.1.13)
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.2.0
|
specifier: 1.2.4
|
||||||
version: 1.2.0(typescript@5.5.3)
|
version: 1.2.4(typescript@5.5.3)
|
||||||
bl:
|
bl:
|
||||||
specifier: 6.0.11
|
specifier: 6.0.11
|
||||||
version: 6.0.11
|
version: 6.0.11
|
||||||
@@ -665,13 +665,13 @@ importers:
|
|||||||
version: 16.4.5
|
version: 16.4.5
|
||||||
drizzle-dbml-generator:
|
drizzle-dbml-generator:
|
||||||
specifier: 0.10.0
|
specifier: 0.10.0
|
||||||
version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))
|
version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.39.1
|
specifier: ^0.39.1
|
||||||
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||||
drizzle-zod:
|
drizzle-zod:
|
||||||
specifier: 0.5.1
|
specifier: 0.5.1
|
||||||
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
|
||||||
hi-base32:
|
hi-base32:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
@@ -4030,8 +4030,8 @@ packages:
|
|||||||
before-after-hook@2.2.3:
|
before-after-hook@2.2.3:
|
||||||
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
||||||
|
|
||||||
better-auth@1.2.0:
|
better-auth@1.2.4:
|
||||||
resolution: {integrity: sha512-eIRGOXfix25bh4fgs8jslZAZssufpIkxfEeEokQu5G4wICoDee1wPctkFb8v80PvhtI4dPm28SuAoZaAdRc6Wg==}
|
resolution: {integrity: sha512-/ZK2jbUjm8JwdeCLFrUWUBmexPyI9PkaLVXWLWtN60sMDHTY8B5G72wcHglo1QMFBaw4G0qFkP5ayl9k6XfDaA==}
|
||||||
|
|
||||||
better-call@1.0.3:
|
better-call@1.0.3:
|
||||||
resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==}
|
resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==}
|
||||||
@@ -5486,8 +5486,8 @@ packages:
|
|||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
kysely@0.27.5:
|
kysely@0.27.6:
|
||||||
resolution: {integrity: sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==}
|
resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
leac@0.6.0:
|
leac@0.6.0:
|
||||||
@@ -10902,7 +10902,7 @@ snapshots:
|
|||||||
|
|
||||||
before-after-hook@2.2.3: {}
|
before-after-hook@2.2.3: {}
|
||||||
|
|
||||||
better-auth@1.2.0(typescript@5.5.3):
|
better-auth@1.2.4(typescript@5.5.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/utils': 0.2.3
|
'@better-auth/utils': 0.2.3
|
||||||
'@better-fetch/fetch': 1.1.15
|
'@better-fetch/fetch': 1.1.15
|
||||||
@@ -10913,7 +10913,7 @@ snapshots:
|
|||||||
better-call: 1.0.3
|
better-call: 1.0.3
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
jose: 5.9.6
|
jose: 5.9.6
|
||||||
kysely: 0.27.5
|
kysely: 0.27.6
|
||||||
nanostores: 0.11.3
|
nanostores: 0.11.3
|
||||||
valibot: 1.0.0-beta.15(typescript@5.5.3)
|
valibot: 1.0.0-beta.15(typescript@5.5.3)
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
@@ -11534,9 +11534,9 @@ snapshots:
|
|||||||
|
|
||||||
drange@1.1.1: {}
|
drange@1.1.1: {}
|
||||||
|
|
||||||
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)):
|
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)):
|
||||||
dependencies:
|
dependencies:
|
||||||
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||||
|
|
||||||
drizzle-kit@0.30.4:
|
drizzle-kit@0.30.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11547,18 +11547,18 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7):
|
drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@types/react': 18.3.5
|
'@types/react': 18.3.5
|
||||||
kysely: 0.27.5
|
kysely: 0.27.6
|
||||||
postgres: 3.4.4
|
postgres: 3.4.4
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
sqlite3: 5.1.7
|
sqlite3: 5.1.7
|
||||||
|
|
||||||
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
|
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
@@ -12450,7 +12450,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
|
||||||
kysely@0.27.5: {}
|
kysely@0.27.6: {}
|
||||||
|
|
||||||
leac@0.6.0: {}
|
leac@0.6.0: {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user