Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a48d4636 | ||
|
|
c34c4b244e | ||
|
|
4991e4b1f2 | ||
|
|
b7c061dcb4 | ||
|
|
701319efdd | ||
|
|
0fdf176648 | ||
|
|
f62d835f57 | ||
|
|
5bb4710952 | ||
|
|
9b71ce9388 | ||
|
|
175d1ec432 | ||
|
|
8454e4f781 | ||
|
|
2e79c7230f | ||
|
|
7ddd3bb8a0 | ||
|
|
b889a9d248 | ||
|
|
fc21c96cd1 | ||
|
|
0341b19c9f | ||
|
|
43095f2435 | ||
|
|
f6128bdf0c | ||
|
|
6be6ec940a | ||
|
|
ba9fc59805 | ||
|
|
63a1039439 | ||
|
|
d52692c6a3 | ||
|
|
b4511ca7a2 | ||
|
|
a3c24f1f2a | ||
|
|
ca599f27f7 | ||
|
|
f36de7b2f5 | ||
|
|
0ce055c001 | ||
|
|
fc011a5661 | ||
|
|
60d6d781be | ||
|
|
9270739eb6 | ||
|
|
87b87b85c0 | ||
|
|
496fd40fa3 | ||
|
|
25fe080582 | ||
|
|
1c41091372 | ||
|
|
9230178005 | ||
|
|
fd092f1248 | ||
|
|
736c186a66 | ||
|
|
3d348ee762 | ||
|
|
6e78f49c2f | ||
|
|
e77b30671b | ||
|
|
244e1227c4 | ||
|
|
9934dac203 | ||
|
|
44ee326057 | ||
|
|
18f892096b | ||
|
|
9954d5b209 | ||
|
|
e0bde5cec9 | ||
|
|
2d4eaeb8b5 | ||
|
|
787506fb6b | ||
|
|
50c8c3a43a | ||
|
|
1f09c06274 | ||
|
|
af11bc8cd2 | ||
|
|
6779dec1ff | ||
|
|
191a6112ce | ||
|
|
1bf518f768 | ||
|
|
79ad0818f5 | ||
|
|
5fadd73732 | ||
|
|
342ff4b589 | ||
|
|
680811357b |
72
.circleci/canary-config.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
version: 2.1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-amd64:
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2004:current
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: Prepare .env file
|
||||||
|
command: |
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
- run:
|
||||||
|
name: Build and push AMD64 image
|
||||||
|
command: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
echo $VERSION
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
docker build --platform linux/amd64 -t dokploy/dokploy:canary-amd64 .
|
||||||
|
docker push dokploy/dokploy:canary-amd64
|
||||||
|
|
||||||
|
build-arm64:
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2004:current
|
||||||
|
resource_class: arm.large
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Prepare .env file
|
||||||
|
command: |
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
- run:
|
||||||
|
name: Build and push ARM64 image
|
||||||
|
command: |
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
docker build --platform linux/arm64 -t dokploy/dokploy:canary-arm64 .
|
||||||
|
docker push dokploy/dokploy:canary-arm64
|
||||||
|
|
||||||
|
combine-manifests:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:stable
|
||||||
|
steps:
|
||||||
|
- setup_remote_docker
|
||||||
|
- run:
|
||||||
|
name: Create and push multi-arch manifest
|
||||||
|
command: |
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
docker manifest create dokploy/dokploy:canary \
|
||||||
|
dokploy/dokploy:canary-amd64 \
|
||||||
|
dokploy/dokploy:canary-arm64
|
||||||
|
docker manifest push dokploy/dokploy:canary
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build-all:
|
||||||
|
jobs:
|
||||||
|
- build-amd64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: feat/circle
|
||||||
|
- build-arm64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: feat/circle
|
||||||
|
- combine-manifests:
|
||||||
|
requires:
|
||||||
|
- build-amd64
|
||||||
|
- build-arm64
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: feat/circle
|
||||||
104
.circleci/config.yml
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
version: 2.1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-amd64:
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2004:current
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: Prepare .env file
|
||||||
|
command: |
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
- run:
|
||||||
|
name: Build and push AMD64 image
|
||||||
|
command: |
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||||
|
TAG="latest"
|
||||||
|
else
|
||||||
|
TAG="canary"
|
||||||
|
fi
|
||||||
|
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
|
||||||
|
docker push dokploy/dokploy:${TAG}-amd64
|
||||||
|
|
||||||
|
build-arm64:
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2004:current
|
||||||
|
resource_class: arm.large
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: Prepare .env file
|
||||||
|
command: |
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
- run:
|
||||||
|
name: Build and push ARM64 image
|
||||||
|
command: |
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||||
|
TAG="latest"
|
||||||
|
else
|
||||||
|
TAG="canary"
|
||||||
|
fi
|
||||||
|
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
|
||||||
|
docker push dokploy/dokploy:${TAG}-arm64
|
||||||
|
|
||||||
|
combine-manifests:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:18.18.0
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- setup_remote_docker
|
||||||
|
- run:
|
||||||
|
name: Create and push multi-arch manifest
|
||||||
|
command: |
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
|
||||||
|
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
echo $VERSION
|
||||||
|
TAG="latest"
|
||||||
|
|
||||||
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
|
dokploy/dokploy:${TAG}-arm64
|
||||||
|
docker manifest push dokploy/dokploy:${TAG}
|
||||||
|
|
||||||
|
docker manifest create dokploy/dokploy:${VERSION} \
|
||||||
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
|
dokploy/dokploy:${TAG}-arm64
|
||||||
|
docker manifest push dokploy/dokploy:${VERSION}
|
||||||
|
else
|
||||||
|
TAG="canary"
|
||||||
|
docker manifest create dokploy/dokploy:${TAG} \
|
||||||
|
dokploy/dokploy:${TAG}-amd64 \
|
||||||
|
dokploy/dokploy:${TAG}-arm64
|
||||||
|
docker manifest push dokploy/dokploy:${TAG}
|
||||||
|
fi
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build-all:
|
||||||
|
jobs:
|
||||||
|
- build-amd64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
|
- build-arm64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
|
- combine-manifests:
|
||||||
|
requires:
|
||||||
|
- build-amd64
|
||||||
|
- build-arm64
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
76
.circleci/main-config.yml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
version: 2.1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-amd64:
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2004:current
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: Prepare .env file
|
||||||
|
command: |
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
- run:
|
||||||
|
name: Build and push AMD64 image
|
||||||
|
command: |
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
docker build --platform linux/amd64 -t dokploy/dokploy:latest-amd64 .
|
||||||
|
docker push dokploy/dokploy:latest-amd64
|
||||||
|
|
||||||
|
build-arm64:
|
||||||
|
machine:
|
||||||
|
image: ubuntu-2004:current
|
||||||
|
resource_class: arm.large
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Prepare .env file
|
||||||
|
command: |
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
- run:
|
||||||
|
name: Build and push ARM64 image
|
||||||
|
command: |
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
docker build --platform linux/arm64 -t dokploy/dokploy:latest-arm64 .
|
||||||
|
docker push dokploy/dokploy:latest-arm64
|
||||||
|
|
||||||
|
combine-manifests:
|
||||||
|
docker:
|
||||||
|
- image: cimg/base:stable
|
||||||
|
steps:
|
||||||
|
- setup_remote_docker
|
||||||
|
- run:
|
||||||
|
name: Create and push multi-arch manifest
|
||||||
|
command: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||||
|
docker manifest create dokploy/dokploy:latest \
|
||||||
|
dokploy/dokploy:latest-amd64 \
|
||||||
|
dokploy/dokploy:latest-arm64
|
||||||
|
docker manifest push dokploy/dokploy:latest
|
||||||
|
|
||||||
|
docker manifest create dokploy/dokploy:${VERSION} \
|
||||||
|
dokploy/dokploy:latest-amd64 \
|
||||||
|
dokploy/dokploy:latest-arm64
|
||||||
|
docker manifest push dokploy/dokploy:${VERSION}
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
build-all:
|
||||||
|
jobs:
|
||||||
|
- build-amd64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: main
|
||||||
|
- build-arm64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: main
|
||||||
|
- combine-manifests:
|
||||||
|
requires:
|
||||||
|
- build-amd64
|
||||||
|
- build-arm64
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: main
|
||||||
43
.github/workflows/pull-request.yml
vendored
@@ -16,29 +16,34 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
node-version: [18.18.0]
|
node-version: [18.18.0]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the code
|
- name: Check out the code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Run format and lint
|
# - name: Run commitlint
|
||||||
run: pnpm biome ci
|
# run: pnpm commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||||
|
|
||||||
- name: Run type check
|
- name: Run format and lint
|
||||||
run: pnpm typecheck
|
run: pnpm biome ci
|
||||||
|
|
||||||
- name: Run Build
|
- name: Run type check
|
||||||
run: pnpm build
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Build
|
||||||
run: pnpm run test
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: pnpm run test
|
||||||
|
|||||||
35
.github/workflows/push.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: Push
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- canary
|
|
||||||
|
|
||||||
env:
|
|
||||||
HUSKY: 0
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push-docker-on-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out the code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Prepare .env file
|
|
||||||
run: |
|
|
||||||
cp .env.production.example .env.production
|
|
||||||
|
|
||||||
- name: Build and push Docker image using custom script
|
|
||||||
run: |
|
|
||||||
chmod +x ./docker/push.sh
|
|
||||||
./docker/push.sh ${{ github.ref_name == 'canary' && 'canary' || '' }}
|
|
||||||
4
.husky/commit-msg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm commitlint --edit $1
|
||||||
@@ -72,7 +72,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
# Install buildpacks
|
# Install buildpacks
|
||||||
RUN 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
|
RUN curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
98
__test__/drop/drop.test.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||||
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
const undici = require("undici");
|
||||||
|
globalThis.File = undici.File as any;
|
||||||
|
globalThis.FileList = undici.FileList as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock("@/server/constants", () => ({
|
||||||
|
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("unzipDrop using real zip files", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root folder", async () => {
|
||||||
|
const appName = "single-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
|
const appName = "folderwithfile";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with multiple root folders", async () => {
|
||||||
|
const appName = "two-folders";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root with a file", async () => {
|
||||||
|
const appName = "nested";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder3")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||||
|
const appName = "folder-with-sibling-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, appName);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
__test__/drop/zips/folder-with-file.zip
Normal file
BIN
__test__/drop/zips/folder-with-sibling-file.zip
Normal file
1
__test__/drop/zips/folder1/folder1.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Gogogogogogo
|
||||||
1
__test__/drop/zips/folder2/folder2.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
gogogogogog
|
||||||
1
__test__/drop/zips/folder3/file3.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
gogogogogogogogogo
|
||||||
BIN
__test__/drop/zips/nested.zip
Normal file
BIN
__test__/drop/zips/single-file.zip
Normal file
1
__test__/drop/zips/test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dsafasdfasdf
|
||||||
BIN
__test__/drop/zips/two-folders.zip
Normal file
@@ -63,6 +63,7 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("file"),
|
type: z.literal("file"),
|
||||||
|
filePath: z.string().min(1, "File path required"),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
})
|
})
|
||||||
.merge(mountSchema),
|
.merge(mountSchema),
|
||||||
@@ -81,7 +82,7 @@ export const AddVolumes = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: serviceType === "compose" ? "file" : "bind",
|
type: serviceType === "compose" ? "file" : "bind",
|
||||||
hostPath: "",
|
hostPath: "",
|
||||||
mountPath: "",
|
mountPath: serviceType === "compose" ? "/" : "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(mySchema),
|
resolver: zodResolver(mySchema),
|
||||||
});
|
});
|
||||||
@@ -125,6 +126,7 @@ export const AddVolumes = ({
|
|||||||
serviceId,
|
serviceId,
|
||||||
content: data.content,
|
content: data.content,
|
||||||
mountPath: data.mountPath,
|
mountPath: data.mountPath,
|
||||||
|
filePath: data.filePath,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
serviceType,
|
serviceType,
|
||||||
})
|
})
|
||||||
@@ -288,41 +290,62 @@ export const AddVolumes = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "file" && (
|
{type === "file" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="content"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Content</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Any content"
|
||||||
|
className="h-64"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="filePath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>File Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Name of the file"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{serviceType !== "compose" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="content"
|
name="mountPath"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Mount Path (In the container)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<Input placeholder="Mount Path" {...field} />
|
||||||
<Textarea
|
|
||||||
placeholder="Any content"
|
|
||||||
className="h-64"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="mountPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Mount Path</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Mount Path" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -73,8 +73,7 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
key={mount.mountId}
|
key={mount.mountId}
|
||||||
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||||
>
|
>
|
||||||
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Mount Type</span>
|
<span className="font-medium">Mount Type</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -91,12 +90,21 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<div className="flex flex-col gap-1">
|
<>
|
||||||
<span className="font-medium">Content</span>
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="font-medium">Content</span>
|
||||||
{mount.content}
|
<span className="text-sm text-muted-foreground">
|
||||||
</span>
|
{mount.content}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">File Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.filePath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{mount.type === "bind" && (
|
{mount.type === "bind" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -118,6 +126,7 @@ export const ShowVolumes = ({ applicationId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
serviceType="application"
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
.object({
|
.object({
|
||||||
type: z.literal("file"),
|
type: z.literal("file"),
|
||||||
content: z.string().optional(),
|
content: z.string().optional(),
|
||||||
|
filePath: z.string().min(1, "File path required"),
|
||||||
})
|
})
|
||||||
.merge(mountSchema),
|
.merge(mountSchema),
|
||||||
]);
|
]);
|
||||||
@@ -58,9 +59,23 @@ interface Props {
|
|||||||
mountId: string;
|
mountId: string;
|
||||||
type: "bind" | "volume" | "file";
|
type: "bind" | "volume" | "file";
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
|
serviceType:
|
||||||
|
| "application"
|
||||||
|
| "postgres"
|
||||||
|
| "redis"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "mysql"
|
||||||
|
| "mariadb"
|
||||||
|
| "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
|
export const UpdateVolume = ({
|
||||||
|
mountId,
|
||||||
|
type,
|
||||||
|
refetch,
|
||||||
|
serviceType,
|
||||||
|
}: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.mounts.one.useQuery(
|
const { data } = api.mounts.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -103,6 +118,7 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
content: data.content || "",
|
content: data.content || "",
|
||||||
mountPath: data.mountPath,
|
mountPath: data.mountPath,
|
||||||
|
filePath: data.filePath || "",
|
||||||
type: "file",
|
type: "file",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -141,6 +157,7 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
|
|||||||
content: data.content,
|
content: data.content,
|
||||||
mountPath: data.mountPath,
|
mountPath: data.mountPath,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
|
filePath: data.filePath,
|
||||||
mountId,
|
mountId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -166,6 +183,11 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
|
|||||||
<DialogDescription>Update the mount</DialogDescription>
|
<DialogDescription>Update the mount</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
{type === "file" && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Updating the mount will recreate the file or directory.
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -211,40 +233,62 @@ export const UpdateVolume = ({ mountId, type, refetch }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "file" && (
|
{type === "file" && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="content"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="content"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Content</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<FormControl>
|
||||||
placeholder="Any content"
|
<Textarea
|
||||||
className="h-64"
|
placeholder="Any content"
|
||||||
|
className="h-64"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="filePath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>File Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
placeholder="Name of the file"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{serviceType !== "compose" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="mountPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Mount Path (In the container)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Mount Path" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="mountPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Mount Path</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Mount Path" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const RefreshToken = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone. This will permanently delete the
|
This action cannot be undone. This will change the refresh token and
|
||||||
domain
|
other tokens will be invalidated.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dropzone } from "@/components/ui/dropzone";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { type UploadFile, uploadFileSchema } from "@/utils/schema";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveDragNDrop = ({ applicationId }: Props) => {
|
||||||
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } =
|
||||||
|
api.application.dropDeployment.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<UploadFile>({
|
||||||
|
defaultValues: {},
|
||||||
|
resolver: zodResolver(uploadFileSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
dropBuildPath: data.dropBuildPath || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
const zip = form.watch("zip");
|
||||||
|
|
||||||
|
const onSubmit = async (values: UploadFile) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("zip", values.zip);
|
||||||
|
formData.append("applicationId", applicationId);
|
||||||
|
if (values.dropBuildPath) {
|
||||||
|
formData.append("dropBuildPath", values.dropBuildPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(formData)
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Deployment saved");
|
||||||
|
await refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to save the deployment");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 ">
|
||||||
|
<div className="md:col-span-2 space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dropBuildPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full ">
|
||||||
|
<FormLabel>Build Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Build Path" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="zip"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full ">
|
||||||
|
<FormLabel>Zip file</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Dropzone
|
||||||
|
{...field}
|
||||||
|
dropMessage="Drop files or click here"
|
||||||
|
accept=".zip"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e instanceof FileList) {
|
||||||
|
field.onChange(e[0]);
|
||||||
|
} else {
|
||||||
|
field.onChange(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
{zip instanceof File && (
|
||||||
|
<div className="flex flex-row gap-4 items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{zip.name} ({zip.size} bytes)
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-fit"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-fit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={!zip}
|
||||||
|
>
|
||||||
|
Deploy{" "}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,8 +7,9 @@ import { api } from "@/utils/api";
|
|||||||
import { GitBranch, LockIcon } from "lucide-react";
|
import { GitBranch, LockIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||||
|
|
||||||
type TabState = "github" | "docker" | "git";
|
type TabState = "github" | "docker" | "git" | "drop";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -62,6 +63,12 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
>
|
>
|
||||||
Git
|
Git
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="drop"
|
||||||
|
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||||
|
>
|
||||||
|
Drop
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="github" className="w-full p-2">
|
<TabsContent value="github" className="w-full p-2">
|
||||||
{haveGithubConfigured ? (
|
{haveGithubConfigured ? (
|
||||||
@@ -89,6 +96,9 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
<TabsContent value="git" className="w-full p-2">
|
<TabsContent value="git" className="w-full p-2">
|
||||||
<SaveGitProvider applicationId={applicationId} />
|
<SaveGitProvider applicationId={applicationId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="drop" className="w-full p-2">
|
||||||
|
<SaveDragNDrop applicationId={applicationId} />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const ShowVolumesCompose = ({ composeId }: Props) => {
|
|||||||
key={mount.mountId}
|
key={mount.mountId}
|
||||||
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Mount Type</span>
|
<span className="font-medium">Mount Type</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -91,12 +91,20 @@ export const ShowVolumesCompose = ({ composeId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<div className="flex flex-col gap-1">
|
<>
|
||||||
<span className="font-medium">Content</span>
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm text-muted-foreground w-40 truncate">
|
<span className="font-medium">Content</span>
|
||||||
{mount.content}
|
<span className="text-sm text-muted-foreground w-40 truncate">
|
||||||
</span>
|
{mount.content}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">File Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.filePath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{mount.type === "bind" && (
|
{mount.type === "bind" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -118,6 +126,7 @@ export const ShowVolumesCompose = ({ composeId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
serviceType="compose"
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Toggle } from "@/components/ui/toggle";
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLink, Globe, Terminal } from "lucide-react";
|
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
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";
|
||||||
@@ -50,7 +50,6 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<DeployCompose composeId={composeId} />
|
<DeployCompose composeId={composeId} />
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
aria-label="Toggle italic"
|
aria-label="Toggle italic"
|
||||||
pressed={data?.autoDeploy || false}
|
pressed={data?.autoDeploy || false}
|
||||||
@@ -67,8 +66,9 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
toast.error("Error to update Auto Deploy");
|
toast.error("Error to update Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center"
|
||||||
>
|
>
|
||||||
Autodeploy
|
Autodeploy {data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
||||||
</Toggle>
|
</Toggle>
|
||||||
<RedbuildCompose composeId={composeId} />
|
<RedbuildCompose composeId={composeId} />
|
||||||
{data?.composeType === "docker-compose" && (
|
{data?.composeType === "docker-compose" && (
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React, { useEffect } from "react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import React, { 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";
|
||||||
@@ -32,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.mariadb.saveEnvironment.useMutation();
|
const { mutateAsync, isLoading } = api.mariadb.saveEnvironment.useMutation();
|
||||||
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
const { data, refetch } = api.mariadb.one.useQuery(
|
||||||
@@ -74,11 +77,26 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|||||||
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>
|
{" "}
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<CardHeader className="flex flex-row w-full items-center justify-between">
|
||||||
<CardDescription>
|
<div>
|
||||||
You can add environment variables to your database.
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
</CardDescription>
|
<CardDescription>
|
||||||
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -95,6 +113,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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, 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("");
|
||||||
@@ -76,10 +77,9 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${hostname}:${port}/${data?.databaseName}`;
|
return `mariadb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export const ShowVolumes = ({ mariadbId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
serviceType="mariadb"
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React, { useEffect } from "react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import React, { 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";
|
||||||
@@ -32,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.mongo.saveEnvironment.useMutation();
|
const { mutateAsync, isLoading } = api.mongo.saveEnvironment.useMutation();
|
||||||
|
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
@@ -74,11 +77,25 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
|||||||
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 className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your database.
|
<CardDescription>
|
||||||
</CardDescription>
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -95,6 +112,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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, 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("");
|
||||||
@@ -77,10 +78,9 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${hostname}:${port}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export const ShowVolumes = ({ mongoId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
serviceType="mongo"
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React, { useEffect } from "react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import React, { 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";
|
||||||
@@ -32,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.mysql.saveEnvironment.useMutation();
|
const { mutateAsync, isLoading } = api.mysql.saveEnvironment.useMutation();
|
||||||
|
|
||||||
const { data, refetch } = api.mysql.one.useQuery(
|
const { data, refetch } = api.mysql.one.useQuery(
|
||||||
@@ -74,11 +77,25 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
|||||||
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 className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your database.
|
<CardDescription>
|
||||||
</CardDescription>
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -95,6 +112,7 @@ export const ShowMysqlEnvironment = ({ mysqlId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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, 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("");
|
||||||
@@ -77,10 +78,9 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${hostname}:${port}/${data?.databaseName}`;
|
return `mysql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
|
|||||||
@@ -86,12 +86,14 @@ export const ShowVolumes = ({ mysqlId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<div className="flex flex-col gap-1">
|
<>
|
||||||
<span className="font-medium">Content</span>
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="font-medium">Content</span>
|
||||||
{mount.content}
|
<span className="text-sm text-muted-foreground">
|
||||||
</span>
|
{mount.content}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{mount.type === "bind" && (
|
{mount.type === "bind" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -113,6 +115,7 @@ export const ShowVolumes = ({ mysqlId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
serviceType="mysql"
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React, { useEffect } from "react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import React, { 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";
|
||||||
@@ -32,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.postgres.saveEnvironment.useMutation();
|
const { mutateAsync, isLoading } = api.postgres.saveEnvironment.useMutation();
|
||||||
|
|
||||||
const { data, refetch } = api.postgres.one.useQuery(
|
const { data, refetch } = api.postgres.one.useQuery(
|
||||||
@@ -74,11 +77,25 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
|||||||
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 className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your database.
|
<CardDescription>
|
||||||
</CardDescription>
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -95,6 +112,7 @@ export const ShowPostgresEnvironment = ({ postgresId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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, 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();
|
||||||
@@ -81,7 +82,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
|
|||||||
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 `postgresql://${data?.databaseUser}:${data?.databasePassword}@${hostname}:${port}/${data?.databaseName}`;
|
return `postgresql://${data?.databaseUser}:${data?.databasePassword}@${ip}:${port}/${data?.databaseName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export const ShowVolumes = ({ postgresId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
serviceType="postgres"
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
} from "@/components/ui/command";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -22,8 +29,23 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Code, Github, Globe, PuzzleIcon } from "lucide-react";
|
import { ScrollArea } from "@radix-ui/react-scroll-area";
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronsUpDown,
|
||||||
|
Code,
|
||||||
|
Github,
|
||||||
|
Globe,
|
||||||
|
PuzzleIcon,
|
||||||
|
SearchIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -34,13 +56,23 @@ interface Props {
|
|||||||
export const AddTemplate = ({ projectId }: Props) => {
|
export const AddTemplate = ({ projectId }: Props) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const { data } = api.compose.templates.useQuery();
|
const { data } = api.compose.templates.useQuery();
|
||||||
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
|
const { data: tags, isLoading: isLoadingTags } =
|
||||||
|
api.compose.getTags.useQuery();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.compose.deployTemplate.useMutation();
|
api.compose.deployTemplate.useMutation();
|
||||||
|
|
||||||
const templates = data?.filter((t) =>
|
const templates =
|
||||||
t.name.toLowerCase().includes(query.toLowerCase()),
|
data?.filter((template) => {
|
||||||
);
|
const matchesTags =
|
||||||
|
selectedTags.length === 0 ||
|
||||||
|
template.tags.some((tag) => selectedTags.includes(tag));
|
||||||
|
const matchesQuery =
|
||||||
|
query === "" ||
|
||||||
|
template.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
return matchesTags && matchesQuery;
|
||||||
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
@@ -62,146 +94,220 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<Input
|
<div className="flex flex-col md:flex-row gap-2">
|
||||||
placeholder="Search Template"
|
<Input
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
placeholder="Search Template"
|
||||||
value={query}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
/>
|
className="w-full"
|
||||||
</div>
|
value={query}
|
||||||
<div className="p-6">
|
/>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 w-full gap-4">
|
<Popover modal={true}>
|
||||||
{templates?.map((template, index) => (
|
<PopoverTrigger asChild>
|
||||||
<div key={`template-${index}`}>
|
<Button
|
||||||
<div
|
variant="outline"
|
||||||
key={template.id}
|
role="combobox"
|
||||||
className="flex flex-col gap-4 border p-6 rounded-lg h-full"
|
className={cn(
|
||||||
|
"md:max-w-[15rem] w-full justify-between !bg-input",
|
||||||
|
// !field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
{isLoadingTags
|
||||||
<div className="flex flex-col items-center gap-2">
|
? "Loading...."
|
||||||
<img
|
: selectedTags.length > 0
|
||||||
src={`/templates/${template.logo}`}
|
? `Selected ${selectedTags.length} tags`
|
||||||
className="size-28 object-contain"
|
: "Select tag"}
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<ChevronsUpDown className="ml-2 h-4 w-4 opacity-50" />
|
||||||
<div className="flex flex-col gap-2 justify-center items-center">
|
</Button>
|
||||||
<div className="flex flex-col gap-2 items-center justify-center">
|
</PopoverTrigger>
|
||||||
<div className="flex flex-row gap-2 flex-wrap">
|
<PopoverContent className="p-0" align="start">
|
||||||
<span className="text-sm font-medium">
|
<Command>
|
||||||
{template.name}
|
<CommandInput placeholder="Search tag..." className="h-9" />
|
||||||
</span>
|
{isLoadingTags && (
|
||||||
<Badge>{template.version}</Badge>
|
<span className="py-6 text-center text-sm">
|
||||||
</div>
|
Loading Tags....
|
||||||
|
</span>
|
||||||
<div className="flex flex-row gap-0">
|
)}
|
||||||
<Link
|
<CommandEmpty>No tags found.</CommandEmpty>
|
||||||
href={template.links.github}
|
<ScrollArea className="h-96 overflow-y-auto">
|
||||||
target="_blank"
|
<CommandGroup>
|
||||||
className={
|
{tags?.map((tag) => {
|
||||||
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
return (
|
||||||
|
<CommandItem
|
||||||
|
value={tag}
|
||||||
|
key={tag}
|
||||||
|
onSelect={() => {
|
||||||
|
if (selectedTags.includes(tag)) {
|
||||||
|
setSelectedTags(
|
||||||
|
selectedTags.filter((t) => t !== tag),
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
>
|
setSelectedTags([...selectedTags, tag]);
|
||||||
<Github className="size-4 text-muted-foreground" />
|
}}
|
||||||
</Link>
|
>
|
||||||
{template.links.website && (
|
{tag}
|
||||||
<Link
|
<CheckIcon
|
||||||
href={template.links.website}
|
className={cn(
|
||||||
target="_blank"
|
"ml-auto h-4 w-4",
|
||||||
className={
|
selectedTags.includes(tag)
|
||||||
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
? "opacity-100"
|
||||||
}
|
: "opacity-0",
|
||||||
>
|
)}
|
||||||
<Globe className="size-4 text-muted-foreground" />
|
/>
|
||||||
</Link>
|
</CommandItem>
|
||||||
)}
|
);
|
||||||
{template.links.docs && (
|
})}
|
||||||
<Link
|
</CommandGroup>
|
||||||
href={template.links.docs}
|
</ScrollArea>
|
||||||
target="_blank"
|
</Command>
|
||||||
className={
|
</PopoverContent>
|
||||||
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
</Popover>
|
||||||
}
|
</div>
|
||||||
>
|
</div>
|
||||||
<Globe className="size-4 text-muted-foreground" />
|
<div className="p-6 w-full">
|
||||||
</Link>
|
{templates.length === 0 ? (
|
||||||
)}
|
<div className="flex justify-center items-center w-full gap-2 min-h-[50vh]">
|
||||||
<Link
|
<SearchIcon className="text-muted-foreground size-6" />
|
||||||
href={`https://github.com/dokploy/dokploy/tree/canary/templates/${template.id}`}
|
<div className="text-xl font-medium text-muted-foreground">
|
||||||
target="_blank"
|
No templates found
|
||||||
className={
|
</div>
|
||||||
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
</div>
|
||||||
}
|
) : (
|
||||||
>
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 w-full gap-4">
|
||||||
<Code className="size-4 text-muted-foreground" />
|
{templates?.map((template, index) => (
|
||||||
</Link>
|
<div key={`template-${index}`}>
|
||||||
</div>
|
<div
|
||||||
<div className="flex flex-row gap-2 flex-wrap justify-center">
|
key={template.id}
|
||||||
{template.tags.map((tag) => (
|
className="flex flex-col gap-4 border p-6 rounded-lg h-full"
|
||||||
<Badge variant="secondary" key={tag}>
|
>
|
||||||
{tag}
|
<div className="flex flex-col gap-4">
|
||||||
</Badge>
|
<div className="flex flex-col items-center gap-2">
|
||||||
))}
|
<img
|
||||||
</div>
|
src={`/templates/${template.logo}`}
|
||||||
</div>
|
className="size-28 object-contain"
|
||||||
|
alt=""
|
||||||
<AlertDialog>
|
/>
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button onSelect={(e) => e.preventDefault()}>
|
|
||||||
Deploy
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
Are you absolutely sure?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will deploy {template.name} template to
|
|
||||||
your project.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
projectId,
|
|
||||||
id: template.id,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success(
|
|
||||||
`${template.name} template created succesfully`,
|
|
||||||
);
|
|
||||||
|
|
||||||
utils.project.one.invalidate({
|
|
||||||
projectId,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
`Error to delete ${template.name} template`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<div className="flex flex-col gap-2">
|
||||||
{template.description}
|
<div className="flex flex-col gap-2 justify-center items-center">
|
||||||
</p>
|
<div className="flex flex-col gap-2 items-center justify-center">
|
||||||
|
<div className="flex flex-row gap-2 flex-wrap">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{template.name}
|
||||||
|
</span>
|
||||||
|
<Badge>{template.version}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-0">
|
||||||
|
<Link
|
||||||
|
href={template.links.github}
|
||||||
|
target="_blank"
|
||||||
|
className={
|
||||||
|
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Github className="size-4 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
{template.links.website && (
|
||||||
|
<Link
|
||||||
|
href={template.links.website}
|
||||||
|
target="_blank"
|
||||||
|
className={
|
||||||
|
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Globe className="size-4 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{template.links.docs && (
|
||||||
|
<Link
|
||||||
|
href={template.links.docs}
|
||||||
|
target="_blank"
|
||||||
|
className={
|
||||||
|
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Globe className="size-4 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
href={`https://github.com/dokploy/dokploy/tree/canary/templates/${template.id}`}
|
||||||
|
target="_blank"
|
||||||
|
className={
|
||||||
|
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Code className="size-4 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 flex-wrap justify-center">
|
||||||
|
{template.tags.map((tag) => (
|
||||||
|
<Badge variant="secondary" key={tag}>
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button onSelect={(e) => e.preventDefault()}>
|
||||||
|
Deploy
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you absolutely sure?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will deploy {template.name} template to
|
||||||
|
your project.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
projectId,
|
||||||
|
id: template.id,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(
|
||||||
|
`${template.name} template created succesfully`,
|
||||||
|
);
|
||||||
|
|
||||||
|
utils.project.one.invalidate({
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
`Error to delete ${template.name} template`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{template.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React, { useEffect } from "react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
|
import React, { 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";
|
||||||
@@ -32,6 +34,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowRedisEnvironment = ({ redisId }: Props) => {
|
export const ShowRedisEnvironment = ({ redisId }: Props) => {
|
||||||
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
const { mutateAsync, isLoading } = api.redis.saveEnvironment.useMutation();
|
const { mutateAsync, isLoading } = api.redis.saveEnvironment.useMutation();
|
||||||
|
|
||||||
const { data, refetch } = api.redis.one.useQuery(
|
const { data, refetch } = api.redis.one.useQuery(
|
||||||
@@ -74,11 +77,25 @@ export const ShowRedisEnvironment = ({ redisId }: Props) => {
|
|||||||
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 className="flex flex-row w-full items-center justify-between">
|
||||||
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
<div>
|
||||||
<CardDescription>
|
<CardTitle className="text-xl">Environment Settings</CardTitle>
|
||||||
You can add environment variables to your database.
|
<CardDescription>
|
||||||
</CardDescription>
|
You can add environment variables to your resource.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Toggle
|
||||||
|
aria-label="Toggle bold"
|
||||||
|
pressed={isEnvVisible}
|
||||||
|
onPressedChange={setIsEnvVisible}
|
||||||
|
>
|
||||||
|
{isEnvVisible ? (
|
||||||
|
<EyeOffIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</Toggle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -95,6 +112,7 @@ export const ShowRedisEnvironment = ({ redisId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="properties"
|
language="properties"
|
||||||
|
disabled={isEnvVisible}
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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, 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("");
|
||||||
@@ -80,7 +81,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
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}@${hostname}:${port}`;
|
return `redis://default:${data?.databasePassword}@${ip}:${port}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export const ShowVolumes = ({ redisId }: Props) => {
|
|||||||
mountId={mount.mountId}
|
mountId={mount.mountId}
|
||||||
type={mount.type}
|
type={mount.type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
|
serviceType="redis"
|
||||||
/>
|
/>
|
||||||
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
<DeleteVolume mountId={mount.mountId} refetch={refetch} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export const AddRegistry = () => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>
|
<Button className="max-sm:w-full">
|
||||||
<Container className="h-4 w-4" />
|
<Container className="h-4 w-4" />
|
||||||
Create Registry
|
Create Registry
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const AddSelfHostedRegistry = () => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>
|
<Button className="max-sm:w-full">
|
||||||
<Container className="h-4 w-4" />
|
<Container className="h-4 w-4" />
|
||||||
Enable Self Hosted Registry
|
Enable Self Hosted Registry
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export const ShowRegistry = () => {
|
|||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Server className="size-8 self-center text-muted-foreground" />
|
<Server className="size-8 self-center text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
To create a cluster is required to set a registry.
|
To create a cluster is required to set a registry.
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||||
<AddSelfHostedRegistry />
|
<AddSelfHostedRegistry />
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
742
components/dashboard/settings/notifications/add-notification.tsx
Normal file
@@ -0,0 +1,742 @@
|
|||||||
|
import {
|
||||||
|
DiscordIcon,
|
||||||
|
SlackIcon,
|
||||||
|
TelegramIcon,
|
||||||
|
} from "@/components/icons/notification-icons";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AlertTriangle, Mail } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const notificationBaseSchema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
|
appDeploy: z.boolean().default(false),
|
||||||
|
appBuildError: z.boolean().default(false),
|
||||||
|
databaseBackup: z.boolean().default(false),
|
||||||
|
dokployRestart: z.boolean().default(false),
|
||||||
|
dockerCleanup: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const notificationSchema = z.discriminatedUnion("type", [
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("slack"),
|
||||||
|
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||||
|
channel: z.string(),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("telegram"),
|
||||||
|
botToken: z.string().min(1, { message: "Bot Token is required" }),
|
||||||
|
chatId: z.string().min(1, { message: "Chat ID is required" }),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("discord"),
|
||||||
|
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("email"),
|
||||||
|
smtpServer: z.string().min(1, { message: "SMTP Server is required" }),
|
||||||
|
smtpPort: z.number().min(1, { message: "SMTP Port is required" }),
|
||||||
|
username: z.string().min(1, { message: "Username is required" }),
|
||||||
|
password: z.string().min(1, { message: "Password is required" }),
|
||||||
|
fromAddress: z.string().min(1, { message: "From Address is required" }),
|
||||||
|
toAddresses: z
|
||||||
|
.array(
|
||||||
|
z.string().min(1, { message: "Email is required" }).email({
|
||||||
|
message: "Email is invalid",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.min(1, { message: "At least one email is required" }),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const notificationsMap = {
|
||||||
|
slack: {
|
||||||
|
icon: <SlackIcon />,
|
||||||
|
label: "Slack",
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
icon: <TelegramIcon />,
|
||||||
|
label: "Telegram",
|
||||||
|
},
|
||||||
|
discord: {
|
||||||
|
icon: <DiscordIcon />,
|
||||||
|
label: "Discord",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
icon: <Mail size={29} className="text-muted-foreground" />,
|
||||||
|
label: "Email",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||||
|
|
||||||
|
export const AddNotification = () => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||||
|
api.notification.testSlackConnection.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
|
||||||
|
api.notification.testTelegramConnection.useMutation();
|
||||||
|
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
|
||||||
|
api.notification.testDiscordConnection.useMutation();
|
||||||
|
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
|
||||||
|
api.notification.testEmailConnection.useMutation();
|
||||||
|
const slackMutation = api.notification.createSlack.useMutation();
|
||||||
|
const telegramMutation = api.notification.createTelegram.useMutation();
|
||||||
|
const discordMutation = api.notification.createDiscord.useMutation();
|
||||||
|
const emailMutation = api.notification.createEmail.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<NotificationSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
type: "slack",
|
||||||
|
webhookUrl: "",
|
||||||
|
channel: "",
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(notificationSchema),
|
||||||
|
});
|
||||||
|
const type = form.watch("type");
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "toAddresses" as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (type === "email") {
|
||||||
|
append("");
|
||||||
|
}
|
||||||
|
}, [type, append]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset();
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
const activeMutation = {
|
||||||
|
slack: slackMutation,
|
||||||
|
telegram: telegramMutation,
|
||||||
|
discord: discordMutation,
|
||||||
|
email: emailMutation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: NotificationSchema) => {
|
||||||
|
const {
|
||||||
|
appBuildError,
|
||||||
|
appDeploy,
|
||||||
|
dokployRestart,
|
||||||
|
databaseBackup,
|
||||||
|
dockerCleanup,
|
||||||
|
} = data;
|
||||||
|
let promise: Promise<unknown> | null = null;
|
||||||
|
if (data.type === "slack") {
|
||||||
|
promise = slackMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: data.webhookUrl,
|
||||||
|
channel: data.channel,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.type === "telegram") {
|
||||||
|
promise = telegramMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
botToken: data.botToken,
|
||||||
|
chatId: data.chatId,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.type === "discord") {
|
||||||
|
promise = discordMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: data.webhookUrl,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.type === "email") {
|
||||||
|
promise = emailMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
smtpServer: data.smtpServer,
|
||||||
|
smtpPort: data.smtpPort,
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
fromAddress: data.fromAddress,
|
||||||
|
toAddresses: data.toAddresses,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promise) {
|
||||||
|
await promise
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Notification Created");
|
||||||
|
form.reset({
|
||||||
|
type: "slack",
|
||||||
|
webhookUrl: "",
|
||||||
|
});
|
||||||
|
setVisible(false);
|
||||||
|
await utils.notification.all.invalidate();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to create a notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
<Button>Add Notification</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add Notification</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create new notifications providers for multiple
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={form.control._defaultValues.type}
|
||||||
|
name="type"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="space-y-3">
|
||||||
|
<FormLabel className="text-muted-foreground">
|
||||||
|
Select a provider
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||||
|
>
|
||||||
|
{Object.entries(notificationsMap).map(([key, value]) => (
|
||||||
|
<FormItem
|
||||||
|
key={key}
|
||||||
|
className="flex w-full items-center space-x-3 space-y-0"
|
||||||
|
>
|
||||||
|
<FormControl className="w-full">
|
||||||
|
<div>
|
||||||
|
<RadioGroupItem
|
||||||
|
value={key}
|
||||||
|
id={key}
|
||||||
|
className="peer sr-only"
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={key}
|
||||||
|
className="flex flex-col gap-2 items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||||
|
>
|
||||||
|
{value.icon}
|
||||||
|
{value.label}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
{activeMutation[field.value].isError && (
|
||||||
|
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
||||||
|
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
||||||
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
|
{activeMutation[field.value].error?.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormLabel className="text-lg font-semibold leading-none tracking-tight">
|
||||||
|
Fill the next fields.
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{type === "slack" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="channel"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Channel</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Channel" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "telegram" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="botToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bot Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="6660491268:AAFMGmajZOVewpMNZCgJr5H7cpXpoZPgvXw"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="chatId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Chat ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="431231869" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "discord" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "email" && (
|
||||||
|
<>
|
||||||
|
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpServer"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>SMTP Server</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="smtp.gmail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>SMTP Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="587"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value) {
|
||||||
|
const port = Number.parseInt(value);
|
||||||
|
if (port > 0 && port < 65536) {
|
||||||
|
field.onChange(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="username" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="******************"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fromAddress"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>From Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="from@example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2 pt-2">
|
||||||
|
<FormLabel>To Addresses</FormLabel>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div
|
||||||
|
key={field.id}
|
||||||
|
className="flex flex-row gap-2 w-full"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`toAddresses.${index}`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="email@example.com"
|
||||||
|
className="w-full"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{type === "email" &&
|
||||||
|
"toAddresses" in form.formState.errors && (
|
||||||
|
<div className="text-sm font-medium text-destructive">
|
||||||
|
{form.formState?.errors?.toAddresses?.root?.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
append("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormLabel className="text-lg font-semibold leading-none tracking-tight">
|
||||||
|
Select the actions.
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appDeploy"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="">
|
||||||
|
<FormLabel>App Deploy</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a app is deployed.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appBuildError"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>App Build Error</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when the build fails.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="databaseBackup"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Database Backup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a database backup is created.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerCleanup"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Docker Cleanup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when the docker cleanup is
|
||||||
|
performed.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dokployRestart"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a dokploy is restarted.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-row gap-2 !justify-between w-full">
|
||||||
|
<Button
|
||||||
|
isLoading={
|
||||||
|
isLoadingSlack ||
|
||||||
|
isLoadingTelegram ||
|
||||||
|
isLoadingDiscord ||
|
||||||
|
isLoadingEmail
|
||||||
|
}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (type === "slack") {
|
||||||
|
await testSlackConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
channel: form.getValues("channel"),
|
||||||
|
});
|
||||||
|
} else if (type === "telegram") {
|
||||||
|
await testTelegramConnection({
|
||||||
|
botToken: form.getValues("botToken"),
|
||||||
|
chatId: form.getValues("chatId"),
|
||||||
|
});
|
||||||
|
} else if (type === "discord") {
|
||||||
|
await testDiscordConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
});
|
||||||
|
} else if (type === "email") {
|
||||||
|
await testEmailConnection({
|
||||||
|
smtpServer: form.getValues("smtpServer"),
|
||||||
|
smtpPort: form.getValues("smtpPort"),
|
||||||
|
username: form.getValues("username"),
|
||||||
|
password: form.getValues("password"),
|
||||||
|
toAddresses: form.getValues("toAddresses"),
|
||||||
|
fromAddress: form.getValues("fromAddress"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toast.success("Connection Success");
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Error to test the provider");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Notification
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
form="hook-form"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { TrashIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
notificationId: string;
|
||||||
|
}
|
||||||
|
export const DeleteNotification = ({ notificationId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.notification.remove.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" isLoading={isLoading}>
|
||||||
|
<TrashIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the
|
||||||
|
notification
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
notificationId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
utils.notification.all.invalidate();
|
||||||
|
toast.success("Notification delete succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to delete notification");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
DiscordIcon,
|
||||||
|
SlackIcon,
|
||||||
|
TelegramIcon,
|
||||||
|
} from "@/components/icons/notification-icons";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { BellRing, Mail } from "lucide-react";
|
||||||
|
import { AddNotification } from "./add-notification";
|
||||||
|
import { DeleteNotification } from "./delete-notification";
|
||||||
|
import { UpdateNotification } from "./update-notification";
|
||||||
|
|
||||||
|
export const ShowNotifications = () => {
|
||||||
|
const { data } = api.notification.all.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">Notifications</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Add your providers to receive notifications, like Discord, Slack,
|
||||||
|
Telegram, Email.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 pt-4">
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<BellRing className="size-8 self-center text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
To send notifications is required to set at least 1 provider.
|
||||||
|
</span>
|
||||||
|
<AddNotification />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="grid lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
{data?.map((notification, index) => (
|
||||||
|
<div
|
||||||
|
key={notification.notificationId}
|
||||||
|
className="flex items-center justify-between border gap-2 p-3.5 rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex flex-row gap-2 items-center w-full ">
|
||||||
|
{notification.notificationType === "slack" && (
|
||||||
|
<SlackIcon className="text-muted-foreground size-6 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{notification.notificationType === "telegram" && (
|
||||||
|
<TelegramIcon className="text-muted-foreground size-8 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{notification.notificationType === "discord" && (
|
||||||
|
<DiscordIcon className="text-muted-foreground size-7 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{notification.notificationType === "email" && (
|
||||||
|
<Mail
|
||||||
|
size={29}
|
||||||
|
className="text-muted-foreground size-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{notification.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-1 w-fit">
|
||||||
|
<UpdateNotification
|
||||||
|
notificationId={notification.notificationId}
|
||||||
|
/>
|
||||||
|
<DeleteNotification
|
||||||
|
notificationId={notification.notificationId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 justify-end w-full items-end">
|
||||||
|
<AddNotification />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,699 @@
|
|||||||
|
import {
|
||||||
|
DiscordIcon,
|
||||||
|
SlackIcon,
|
||||||
|
TelegramIcon,
|
||||||
|
} from "@/components/icons/notification-icons";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Mail, PenBoxIcon } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
type NotificationSchema,
|
||||||
|
notificationSchema,
|
||||||
|
} from "./add-notification";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
notificationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateNotification = ({ notificationId }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data, refetch } = api.notification.one.useQuery(
|
||||||
|
{
|
||||||
|
notificationId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!notificationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||||
|
api.notification.testSlackConnection.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
|
||||||
|
api.notification.testTelegramConnection.useMutation();
|
||||||
|
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
|
||||||
|
api.notification.testDiscordConnection.useMutation();
|
||||||
|
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
|
||||||
|
api.notification.testEmailConnection.useMutation();
|
||||||
|
const slackMutation = api.notification.updateSlack.useMutation();
|
||||||
|
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||||
|
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||||
|
const emailMutation = api.notification.updateEmail.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<NotificationSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
type: "slack",
|
||||||
|
webhookUrl: "",
|
||||||
|
channel: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(notificationSchema),
|
||||||
|
});
|
||||||
|
const type = form.watch("type");
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "toAddresses" as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
if (data.notificationType === "slack") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
webhookUrl: data.slack?.webhookUrl,
|
||||||
|
channel: data.slack?.channel || "",
|
||||||
|
name: data.name,
|
||||||
|
type: data.notificationType,
|
||||||
|
});
|
||||||
|
} else if (data.notificationType === "telegram") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
botToken: data.telegram?.botToken,
|
||||||
|
chatId: data.telegram?.chatId,
|
||||||
|
type: data.notificationType,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.notificationType === "discord") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
type: data.notificationType,
|
||||||
|
webhookUrl: data.discord?.webhookUrl,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (data.notificationType === "email") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: data.appBuildError,
|
||||||
|
appDeploy: data.appDeploy,
|
||||||
|
dokployRestart: data.dokployRestart,
|
||||||
|
databaseBackup: data.databaseBackup,
|
||||||
|
type: data.notificationType,
|
||||||
|
smtpServer: data.email?.smtpServer,
|
||||||
|
smtpPort: data.email?.smtpPort,
|
||||||
|
username: data.email?.username,
|
||||||
|
password: data.email?.password,
|
||||||
|
toAddresses: data.email?.toAddresses,
|
||||||
|
fromAddress: data.email?.fromAddress,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: data.dockerCleanup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: NotificationSchema) => {
|
||||||
|
const {
|
||||||
|
appBuildError,
|
||||||
|
appDeploy,
|
||||||
|
dokployRestart,
|
||||||
|
databaseBackup,
|
||||||
|
dockerCleanup,
|
||||||
|
} = formData;
|
||||||
|
let promise: Promise<unknown> | null = null;
|
||||||
|
if (formData?.type === "slack" && data?.slackId) {
|
||||||
|
promise = slackMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: formData.webhookUrl,
|
||||||
|
channel: formData.channel,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
slackId: data?.slackId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (formData.type === "telegram" && data?.telegramId) {
|
||||||
|
promise = telegramMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
botToken: formData.botToken,
|
||||||
|
chatId: formData.chatId,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
telegramId: data?.telegramId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (formData.type === "discord" && data?.discordId) {
|
||||||
|
promise = discordMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: formData.webhookUrl,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
discordId: data?.discordId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
} else if (formData.type === "email" && data?.emailId) {
|
||||||
|
promise = emailMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
smtpServer: formData.smtpServer,
|
||||||
|
smtpPort: formData.smtpPort,
|
||||||
|
username: formData.username,
|
||||||
|
password: formData.password,
|
||||||
|
fromAddress: formData.fromAddress,
|
||||||
|
toAddresses: formData.toAddresses,
|
||||||
|
name: formData.name,
|
||||||
|
notificationId: notificationId,
|
||||||
|
emailId: data?.emailId,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promise) {
|
||||||
|
await promise
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Notification Updated");
|
||||||
|
await utils.notification.all.invalidate();
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to update a notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Notification</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Update the current notification config
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 ">
|
||||||
|
<div className="flex flex-row gap-2 w-full items-center">
|
||||||
|
<div className="flex flex-row gap-2 items-center w-full ">
|
||||||
|
<FormLabel className="text-lg font-semibold leading-none tracking-tight flex">
|
||||||
|
{data?.notificationType === "slack"
|
||||||
|
? "Slack"
|
||||||
|
: data?.notificationType === "telegram"
|
||||||
|
? "Telegram"
|
||||||
|
: data?.notificationType === "discord"
|
||||||
|
? "Discord"
|
||||||
|
: "Email"}
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
{data?.notificationType === "slack" && (
|
||||||
|
<SlackIcon className="text-muted-foreground size-6 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{data?.notificationType === "telegram" && (
|
||||||
|
<TelegramIcon className="text-muted-foreground size-8 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{data?.notificationType === "discord" && (
|
||||||
|
<DiscordIcon className="text-muted-foreground size-7 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
{data?.notificationType === "email" && (
|
||||||
|
<Mail
|
||||||
|
size={29}
|
||||||
|
className="text-muted-foreground size-6 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Name" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{type === "slack" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="channel"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Channel</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Channel" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "telegram" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="botToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bot Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="6660491268:AAFMGmajZOVewpMNZCgJr5H7cpXpoZPgvXw"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="chatId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Chat ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="431231869" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "discord" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === "email" && (
|
||||||
|
<>
|
||||||
|
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpServer"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>SMTP Server</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="smtp.gmail.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="smtpPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>SMTP Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="587"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value) {
|
||||||
|
const port = Number.parseInt(value);
|
||||||
|
if (port > 0 && port < 65536) {
|
||||||
|
field.onChange(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="username" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="******************"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fromAddress"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>From Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="from@example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2 pt-2">
|
||||||
|
<FormLabel>To Addresses</FormLabel>
|
||||||
|
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<div
|
||||||
|
key={field.id}
|
||||||
|
className="flex flex-row gap-2 w-full"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`toAddresses.${index}`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="email@example.com"
|
||||||
|
className="w-full"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{type === "email" &&
|
||||||
|
"toAddresses" in form.formState.errors && (
|
||||||
|
<div className="text-sm font-medium text-destructive">
|
||||||
|
{form.formState?.errors?.toAddresses?.root?.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
append("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormLabel className="text-lg font-semibold leading-none tracking-tight">
|
||||||
|
Select the actions.
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={form.control._defaultValues.appDeploy}
|
||||||
|
name="appDeploy"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>App Deploy</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a app is deployed.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={form.control._defaultValues.appBuildError}
|
||||||
|
name="appBuildError"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>App Builder Error</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when the build fails.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="databaseBackup"
|
||||||
|
defaultValue={form.control._defaultValues.databaseBackup}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Database Backup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a database backup is created.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerCleanup"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Docker Cleanup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when the docker cleanup is
|
||||||
|
performed.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={form.control._defaultValues.dokployRestart}
|
||||||
|
name="dokployRestart"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a dokploy is restarted.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-row gap-2 !justify-between w-full">
|
||||||
|
<Button
|
||||||
|
isLoading={
|
||||||
|
isLoadingSlack ||
|
||||||
|
isLoadingTelegram ||
|
||||||
|
isLoadingDiscord ||
|
||||||
|
isLoadingEmail
|
||||||
|
}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (type === "slack") {
|
||||||
|
await testSlackConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
channel: form.getValues("channel"),
|
||||||
|
});
|
||||||
|
} else if (type === "telegram") {
|
||||||
|
await testTelegramConnection({
|
||||||
|
botToken: form.getValues("botToken"),
|
||||||
|
chatId: form.getValues("chatId"),
|
||||||
|
});
|
||||||
|
} else if (type === "discord") {
|
||||||
|
await testDiscordConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
});
|
||||||
|
} else if (type === "email") {
|
||||||
|
await testEmailConnection({
|
||||||
|
smtpServer: form.getValues("smtpServer"),
|
||||||
|
smtpPort: form.getValues("smtpPort"),
|
||||||
|
username: form.getValues("username"),
|
||||||
|
password: form.getValues("password"),
|
||||||
|
toAddresses: form.getValues("toAddresses"),
|
||||||
|
fromAddress: form.getValues("fromAddress"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
toast.success("Connection Success");
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Error to test the provider");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Notification
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
form="hook-form"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
90
components/icons/notification-icons.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
export const SlackIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 2447.6 2452.5"
|
||||||
|
className={cn("size-8", className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g clipRule="evenodd" fillRule="evenodd">
|
||||||
|
<path
|
||||||
|
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
||||||
|
fill="#36c5f0"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
||||||
|
fill="#2eb67d"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
||||||
|
fill="#ecb22e"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
||||||
|
fill="#e01e5a"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TelegramIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
width="48px"
|
||||||
|
height="48px"
|
||||||
|
className={cn("size-9", className)}
|
||||||
|
>
|
||||||
|
<linearGradient
|
||||||
|
id="BiF7D16UlC0RZ_VqXJHnXa"
|
||||||
|
x1="9.858"
|
||||||
|
x2="38.142"
|
||||||
|
y1="9.858"
|
||||||
|
y2="38.142"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0" stopColor="#33bef0" />
|
||||||
|
<stop offset="1" stopColor="#0a85d9" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
fill="url(#BiF7D16UlC0RZ_VqXJHnXa)"
|
||||||
|
d="M44,24c0,11.045-8.955,20-20,20S4,35.045,4,24S12.955,4,24,4S44,12.955,44,24z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.119,23.466c8.155-3.695,17.733-7.704,19.208-8.284c3.252-1.279,4.67,0.028,4.448,2.113 c-0.273,2.555-1.567,9.99-2.363,15.317c-0.466,3.117-2.154,4.072-4.059,2.863c-1.445-0.917-6.413-4.17-7.72-5.282 c-0.891-0.758-1.512-1.608-0.88-2.474c0.185-0.253,0.658-0.763,0.921-1.017c1.319-1.278,1.141-1.553-0.454-0.412 c-0.19,0.136-1.292,0.935-1.745,1.237c-1.11,0.74-2.131,0.78-3.862,0.192c-1.416-0.481-2.776-0.852-3.634-1.223 C8.794,25.983,8.34,24.272,10.119,23.466z"
|
||||||
|
opacity=".05"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M10.836,23.591c7.572-3.385,16.884-7.264,18.246-7.813c3.264-1.318,4.465-0.536,4.114,2.011 c-0.326,2.358-1.483,9.654-2.294,14.545c-0.478,2.879-1.874,3.513-3.692,2.337c-1.139-0.734-5.723-3.754-6.835-4.633 c-0.86-0.679-1.751-1.463-0.71-2.598c0.348-0.379,2.27-2.234,3.707-3.614c0.833-0.801,0.536-1.196-0.469-0.508 c-1.843,1.263-4.858,3.262-5.396,3.625c-1.025,0.69-1.988,0.856-3.664,0.329c-1.321-0.416-2.597-0.819-3.262-1.078 C9.095,25.618,9.075,24.378,10.836,23.591z"
|
||||||
|
opacity=".07"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
d="M11.553,23.717c6.99-3.075,16.035-6.824,17.284-7.343c3.275-1.358,4.28-1.098,3.779,1.91 c-0.36,2.162-1.398,9.319-2.226,13.774c-0.491,2.642-1.593,2.955-3.325,1.812c-0.833-0.55-5.038-3.331-5.951-3.984 c-0.833-0.595-1.982-1.311-0.541-2.721c0.513-0.502,3.874-3.712,6.493-6.21c0.343-0.328-0.088-0.867-0.484-0.604 c-3.53,2.341-8.424,5.59-9.047,6.013c-0.941,0.639-1.845,0.932-3.467,0.466c-1.226-0.352-2.423-0.772-2.889-0.932 C9.384,25.282,9.81,24.484,11.553,23.717z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DiscordIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
width="48px"
|
||||||
|
height="48px"
|
||||||
|
className={cn("size-9", className)}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#536dfe"
|
||||||
|
d="M39.248,10.177c-2.804-1.287-5.812-2.235-8.956-2.778c-0.057-0.01-0.114,0.016-0.144,0.068 c-0.387,0.688-0.815,1.585-1.115,2.291c-3.382-0.506-6.747-0.506-10.059,0c-0.3-0.721-0.744-1.603-1.133-2.291 c-0.03-0.051-0.087-0.077-0.144-0.068c-3.143,0.541-6.15,1.489-8.956,2.778c-0.024,0.01-0.045,0.028-0.059,0.051 c-5.704,8.522-7.267,16.835-6.5,25.044c0.003,0.04,0.026,0.079,0.057,0.103c3.763,2.764,7.409,4.442,10.987,5.554 c0.057,0.017,0.118-0.003,0.154-0.051c0.846-1.156,1.601-2.374,2.248-3.656c0.038-0.075,0.002-0.164-0.076-0.194 c-1.197-0.454-2.336-1.007-3.432-1.636c-0.087-0.051-0.094-0.175-0.014-0.234c0.231-0.173,0.461-0.353,0.682-0.534 c0.04-0.033,0.095-0.04,0.142-0.019c7.201,3.288,14.997,3.288,22.113,0c0.047-0.023,0.102-0.016,0.144,0.017 c0.22,0.182,0.451,0.363,0.683,0.536c0.08,0.059,0.075,0.183-0.012,0.234c-1.096,0.641-2.236,1.182-3.434,1.634 c-0.078,0.03-0.113,0.12-0.075,0.196c0.661,1.28,1.415,2.498,2.246,3.654c0.035,0.049,0.097,0.07,0.154,0.052 c3.595-1.112,7.241-2.79,11.004-5.554c0.033-0.024,0.054-0.061,0.057-0.101c0.917-9.491-1.537-17.735-6.505-25.044 C39.293,10.205,39.272,10.187,39.248,10.177z M16.703,30.273c-2.168,0-3.954-1.99-3.954-4.435s1.752-4.435,3.954-4.435 c2.22,0,3.989,2.008,3.954,4.435C20.658,28.282,18.906,30.273,16.703,30.273z M31.324,30.273c-2.168,0-3.954-1.99-3.954-4.435 s1.752-4.435,3.954-4.435c2.22,0,3.989,2.008,3.954,4.435C35.278,28.282,33.544,30.273,31.324,30.273z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -65,6 +65,12 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: Server,
|
icon: Server,
|
||||||
href: "/dashboard/settings/cluster",
|
href: "/dashboard/settings/cluster",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Notifications",
|
||||||
|
label: "",
|
||||||
|
icon: Bell,
|
||||||
|
href: "/dashboard/settings/notifications",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
@@ -78,6 +84,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
Bell,
|
||||||
Database,
|
Database,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Route,
|
Route,
|
||||||
|
|||||||
79
components/ui/dropzone.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FolderIcon } from "lucide-react";
|
||||||
|
import React, { type ChangeEvent, useRef } from "react";
|
||||||
|
|
||||||
|
interface DropzoneProps
|
||||||
|
extends Omit<
|
||||||
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
"value" | "onChange"
|
||||||
|
> {
|
||||||
|
classNameWrapper?: string;
|
||||||
|
className?: string;
|
||||||
|
dropMessage: string;
|
||||||
|
onChange: (acceptedFiles: FileList | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
|
||||||
|
({ className, classNameWrapper, dropMessage, onChange, ...props }, ref) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
// Function to handle drag over event
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to handle drop event
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const { files } = e.dataTransfer;
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.files = files;
|
||||||
|
onChange(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to simulate a click on the file input element
|
||||||
|
const handleButtonClick = () => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-2 border-dashed bg-muted/20 hover:cursor-pointer hover:border-muted-foreground/50 ",
|
||||||
|
classNameWrapper,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
className="flex flex-col items-center justify-center space-y-2 px-2 py-4 text-xs h-96"
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground">
|
||||||
|
<span className="font-medium text-xl flex items-center gap-2">
|
||||||
|
<FolderIcon className="size-6 text-muted-foreground" />
|
||||||
|
{dropMessage}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={undefined}
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
className={cn("hidden", className)}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange(e.target.files)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
72
drizzle/0019_heavy_freak.sql
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE "public"."notificationType" AS ENUM('slack', 'telegram', 'discord', 'email');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "discord" (
|
||||||
|
"discordId" text PRIMARY KEY NOT NULL,
|
||||||
|
"webhookUrl" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "email" (
|
||||||
|
"emailId" text PRIMARY KEY NOT NULL,
|
||||||
|
"smtpServer" text NOT NULL,
|
||||||
|
"smtpPort" integer NOT NULL,
|
||||||
|
"username" text NOT NULL,
|
||||||
|
"password" text NOT NULL,
|
||||||
|
"fromAddress" text NOT NULL,
|
||||||
|
"toAddress" text[] NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "notification" (
|
||||||
|
"notificationId" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"appDeploy" boolean DEFAULT false NOT NULL,
|
||||||
|
"userJoin" boolean DEFAULT false NOT NULL,
|
||||||
|
"appBuildError" boolean DEFAULT false NOT NULL,
|
||||||
|
"databaseBackup" boolean DEFAULT false NOT NULL,
|
||||||
|
"dokployRestart" boolean DEFAULT false NOT NULL,
|
||||||
|
"notificationType" "notificationType" NOT NULL,
|
||||||
|
"createdAt" text NOT NULL,
|
||||||
|
"slackId" text,
|
||||||
|
"telegramId" text,
|
||||||
|
"discordId" text,
|
||||||
|
"emailId" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "slack" (
|
||||||
|
"slackId" text PRIMARY KEY NOT NULL,
|
||||||
|
"webhookUrl" text NOT NULL,
|
||||||
|
"channel" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "telegram" (
|
||||||
|
"telegramId" text PRIMARY KEY NOT NULL,
|
||||||
|
"botToken" text NOT NULL,
|
||||||
|
"chatId" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "notification" ADD CONSTRAINT "notification_slackId_slack_slackId_fk" FOREIGN KEY ("slackId") REFERENCES "public"."slack"("slackId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "notification" ADD CONSTRAINT "notification_telegramId_telegram_telegramId_fk" FOREIGN KEY ("telegramId") REFERENCES "public"."telegram"("telegramId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "notification" ADD CONSTRAINT "notification_discordId_discord_discordId_fk" FOREIGN KEY ("discordId") REFERENCES "public"."discord"("discordId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "notification" ADD CONSTRAINT "notification_emailId_email_emailId_fk" FOREIGN KEY ("emailId") REFERENCES "public"."email"("emailId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
1
drizzle/0020_fantastic_slapstick.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "notification" ADD COLUMN "dockerCleanup" boolean DEFAULT false NOT NULL;
|
||||||
1
drizzle/0021_premium_sebastian_shaw.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "notification" DROP COLUMN IF EXISTS "userJoin";
|
||||||
1
drizzle/0022_warm_colonel_america.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "sourceType" ADD VALUE 'drop';
|
||||||
1
drizzle/0023_icy_maverick.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "dropBuildPath" text;
|
||||||
1
drizzle/0024_dapper_supernaut.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "mount" ADD COLUMN "filePath" text;
|
||||||
2919
drizzle/meta/0019_snapshot.json
Normal file
2926
drizzle/meta/0020_snapshot.json
Normal file
2919
drizzle/meta/0021_snapshot.json
Normal file
2920
drizzle/meta/0022_snapshot.json
Normal file
2926
drizzle/meta/0023_snapshot.json
Normal file
2932
drizzle/meta/0024_snapshot.json
Normal file
@@ -134,6 +134,48 @@
|
|||||||
"when": 1719928377858,
|
"when": 1719928377858,
|
||||||
"tag": "0018_careful_killmonger",
|
"tag": "0018_careful_killmonger",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721110706912,
|
||||||
|
"tag": "0019_heavy_freak",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721363861686,
|
||||||
|
"tag": "0020_fantastic_slapstick",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721370423752,
|
||||||
|
"tag": "0021_premium_sebastian_shaw",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 22,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721531163852,
|
||||||
|
"tag": "0022_warm_colonel_america",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 23,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721542782659,
|
||||||
|
"tag": "0023_icy_maverick",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 24,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1721603595092,
|
||||||
|
"tag": "0024_dapper_supernaut",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
2
emails/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
113
emails/emails/build-failed.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
projectName: string;
|
||||||
|
applicationName: string;
|
||||||
|
applicationType: string;
|
||||||
|
errorMessage: string;
|
||||||
|
buildLink: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BuildFailedEmail = ({
|
||||||
|
projectName = "dokploy",
|
||||||
|
applicationName = "frontend",
|
||||||
|
applicationType = "application",
|
||||||
|
errorMessage = "Error array.length is not a function",
|
||||||
|
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
|
||||||
|
date = "2023-05-01T00:00:00.000Z",
|
||||||
|
}: TemplateProps) => {
|
||||||
|
const previewText = `Build failed for ${applicationName}`;
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={
|
||||||
|
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||||
|
}
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Build failed for <strong>{applicationName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello,
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Your build for <strong>{applicationName}</strong> failed. Please
|
||||||
|
check the error message below.
|
||||||
|
</Text>
|
||||||
|
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Details: </Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Project Name: <strong>{projectName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Application Name: <strong>{applicationName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Application Type: <strong>{applicationType}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Date: <strong>{date}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Reason: </Text>
|
||||||
|
<Text className="text-[12px] leading-[24px]">{errorMessage}</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[32px] mb-[32px]">
|
||||||
|
<Button
|
||||||
|
href={buildLink}
|
||||||
|
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
|
||||||
|
>
|
||||||
|
View build
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
or copy and paste this URL into your browser:{" "}
|
||||||
|
<Link href={buildLink} className="text-blue-600 no-underline">
|
||||||
|
{buildLink}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BuildFailedEmail;
|
||||||
106
emails/emails/build-success.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
projectName: string;
|
||||||
|
applicationName: string;
|
||||||
|
applicationType: string;
|
||||||
|
buildLink: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BuildSuccessEmail = ({
|
||||||
|
projectName = "dokploy",
|
||||||
|
applicationName = "frontend",
|
||||||
|
applicationType = "application",
|
||||||
|
buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test",
|
||||||
|
date = "2023-05-01T00:00:00.000Z",
|
||||||
|
}: TemplateProps) => {
|
||||||
|
const previewText = `Build success for ${applicationName}`;
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={
|
||||||
|
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||||
|
}
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Build success for <strong>{applicationName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello,
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Your build for <strong>{applicationName}</strong> was successful
|
||||||
|
</Text>
|
||||||
|
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Details: </Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Project Name: <strong>{projectName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Application Name: <strong>{applicationName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Application Type: <strong>{applicationType}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Date: <strong>{date}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[32px] mb-[32px]">
|
||||||
|
<Button
|
||||||
|
href={buildLink}
|
||||||
|
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
|
||||||
|
>
|
||||||
|
View build
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
or copy and paste this URL into your browser:{" "}
|
||||||
|
<Link href={buildLink} className="text-blue-600 no-underline">
|
||||||
|
{buildLink}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BuildSuccessEmail;
|
||||||
105
emails/emails/database-backup.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
projectName: string;
|
||||||
|
applicationName: string;
|
||||||
|
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||||
|
type: "error" | "success";
|
||||||
|
errorMessage?: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DatabaseBackupEmail = ({
|
||||||
|
projectName = "dokploy",
|
||||||
|
applicationName = "frontend",
|
||||||
|
databaseType = "postgres",
|
||||||
|
type = "success",
|
||||||
|
errorMessage,
|
||||||
|
date = "2023-05-01T00:00:00.000Z",
|
||||||
|
}: TemplateProps) => {
|
||||||
|
const previewText = `Database backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`;
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Head />
|
||||||
|
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={
|
||||||
|
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||||
|
}
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Database backup for <strong>{applicationName}</strong>
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello,
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Your database backup for <strong>{applicationName}</strong> was{" "}
|
||||||
|
{type === "success"
|
||||||
|
? "successful ✅"
|
||||||
|
: "failed Please check the error message below. ❌"}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Details: </Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Project Name: <strong>{projectName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Application Name: <strong>{applicationName}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Database Type: <strong>{databaseType}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Date: <strong>{date}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
{type === "error" && errorMessage ? (
|
||||||
|
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Reason: </Text>
|
||||||
|
<Text className="text-[12px] leading-[24px]">
|
||||||
|
{errorMessage || "Error message not provided"}
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
) : null}
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatabaseBackupEmail;
|
||||||
81
emails/emails/docker-cleanup.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
message: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DockerCleanupEmail = ({
|
||||||
|
message = "Docker cleanup for dokploy",
|
||||||
|
date = "2023-05-01T00:00:00.000Z",
|
||||||
|
}: TemplateProps) => {
|
||||||
|
const previewText = "Docker cleanup for dokploy";
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Head />
|
||||||
|
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={
|
||||||
|
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||||
|
}
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Docker cleanup for <strong>dokploy</strong>
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello,
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
The docker cleanup for <strong>dokploy</strong> was successful ✅
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Details: </Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Message: <strong>{message}</strong>
|
||||||
|
</Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Date: <strong>{date}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DockerCleanupEmail;
|
||||||
75
emails/emails/dokploy-restart.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DokployRestartEmail = ({
|
||||||
|
date = "2023-05-01T00:00:00.000Z",
|
||||||
|
}: TemplateProps) => {
|
||||||
|
const previewText = "Your dokploy server was restarted";
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Head />
|
||||||
|
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={
|
||||||
|
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||||
|
}
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Dokploy Server Restart
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello,
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Your dokploy server was restarted ✅
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||||
|
<Text className="!leading-3 font-bold">Details: </Text>
|
||||||
|
<Text className="!leading-3">
|
||||||
|
Date: <strong>{date}</strong>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DokployRestartEmail;
|
||||||
98
emails/emails/invitation.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Hr,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface VercelInviteUserEmailProps {
|
||||||
|
inviteLink: string;
|
||||||
|
toEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InvitationEmail = ({
|
||||||
|
inviteLink,
|
||||||
|
toEmail,
|
||||||
|
}: VercelInviteUserEmailProps) => {
|
||||||
|
const previewText = "Join to Dokploy";
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={
|
||||||
|
"https://raw.githubusercontent.com/Dokploy/dokploy/canary/logo.png"
|
||||||
|
}
|
||||||
|
width="100"
|
||||||
|
height="50"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Join to <strong>Dokploy</strong>
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello,
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
You have been invited to join <strong>Dokploy</strong>, a platform
|
||||||
|
that helps for deploying your apps to the cloud.
|
||||||
|
</Text>
|
||||||
|
<Section className="text-center mt-[32px] mb-[32px]">
|
||||||
|
<Button
|
||||||
|
href={inviteLink}
|
||||||
|
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
|
||||||
|
>
|
||||||
|
Join the team 🚀
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
or copy and paste this URL into your browser:{" "}
|
||||||
|
<Link href={inviteLink} className="text-blue-600 no-underline">
|
||||||
|
https://dokploy.com
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
|
||||||
|
<Text className="text-[#666666] text-[12px] leading-[24px]">
|
||||||
|
This invitation was intended for {toEmail}. This invite was sent
|
||||||
|
from <strong className="text-black">dokploy.com</strong>. If you
|
||||||
|
were not expecting this invitation, you can ignore this email. If
|
||||||
|
you are concerned about your account's safety, please reply to
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvitationEmail;
|
||||||
150
emails/emails/notion-magic-link.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface NotionMagicLinkEmailProps {
|
||||||
|
loginCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = process.env.VERCEL_URL
|
||||||
|
? `https://${process.env.VERCEL_URL}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
export const NotionMagicLinkEmail = ({
|
||||||
|
loginCode,
|
||||||
|
}: NotionMagicLinkEmailProps) => (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>Log in with this magic link</Preview>
|
||||||
|
<Body style={main}>
|
||||||
|
<Container style={container}>
|
||||||
|
<Heading style={h1}>Login</Heading>
|
||||||
|
<Link
|
||||||
|
href="https://notion.so"
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
...link,
|
||||||
|
display: "block",
|
||||||
|
marginBottom: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Click here to log in with this magic link
|
||||||
|
</Link>
|
||||||
|
<Text style={{ ...text, marginBottom: "14px" }}>
|
||||||
|
Or, copy and paste this temporary login code:
|
||||||
|
</Text>
|
||||||
|
<code style={code}>{loginCode}</code>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
...text,
|
||||||
|
color: "#ababab",
|
||||||
|
marginTop: "14px",
|
||||||
|
marginBottom: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
If you didn't try to login, you can safely ignore this email.
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
...text,
|
||||||
|
color: "#ababab",
|
||||||
|
marginTop: "12px",
|
||||||
|
marginBottom: "38px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hint: You can set a permanent password in Settings & members → My
|
||||||
|
account.
|
||||||
|
</Text>
|
||||||
|
<Img
|
||||||
|
src={`${baseUrl}/static/notion-logo.png`}
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
alt="Notion's Logo"
|
||||||
|
/>
|
||||||
|
<Text style={footer}>
|
||||||
|
<Link
|
||||||
|
href="https://notion.so"
|
||||||
|
target="_blank"
|
||||||
|
style={{ ...link, color: "#898989" }}
|
||||||
|
>
|
||||||
|
Notion.so
|
||||||
|
</Link>
|
||||||
|
, the all-in-one-workspace
|
||||||
|
<br />
|
||||||
|
for your notes, tasks, wikis, and databases.
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
|
||||||
|
NotionMagicLinkEmail.PreviewProps = {
|
||||||
|
loginCode: "sparo-ndigo-amurt-secan",
|
||||||
|
} as NotionMagicLinkEmailProps;
|
||||||
|
|
||||||
|
export default NotionMagicLinkEmail;
|
||||||
|
|
||||||
|
const main = {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
paddingLeft: "12px",
|
||||||
|
paddingRight: "12px",
|
||||||
|
margin: "0 auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
const h1 = {
|
||||||
|
color: "#333",
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||||
|
fontSize: "24px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
margin: "40px 0",
|
||||||
|
padding: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const link = {
|
||||||
|
color: "#2754C5",
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||||
|
fontSize: "14px",
|
||||||
|
textDecoration: "underline",
|
||||||
|
};
|
||||||
|
|
||||||
|
const text = {
|
||||||
|
color: "#333",
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||||
|
fontSize: "14px",
|
||||||
|
margin: "24px 0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footer = {
|
||||||
|
color: "#898989",
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||||
|
fontSize: "12px",
|
||||||
|
lineHeight: "22px",
|
||||||
|
marginTop: "12px",
|
||||||
|
marginBottom: "24px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = {
|
||||||
|
display: "inline-block",
|
||||||
|
padding: "16px 4.5%",
|
||||||
|
width: "90.5%",
|
||||||
|
backgroundColor: "#f4f4f4",
|
||||||
|
borderRadius: "5px",
|
||||||
|
border: "1px solid #eee",
|
||||||
|
color: "#333",
|
||||||
|
};
|
||||||
158
emails/emails/plaid-verify-identity.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Section,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface PlaidVerifyIdentityEmailProps {
|
||||||
|
validationCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = process.env.VERCEL_URL
|
||||||
|
? `https://${process.env.VERCEL_URL}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
export const PlaidVerifyIdentityEmail = ({
|
||||||
|
validationCode,
|
||||||
|
}: PlaidVerifyIdentityEmailProps) => (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Body style={main}>
|
||||||
|
<Container style={container}>
|
||||||
|
<Img
|
||||||
|
src={`${baseUrl}/static/plaid-logo.png`}
|
||||||
|
width="212"
|
||||||
|
height="88"
|
||||||
|
alt="Plaid"
|
||||||
|
style={logo}
|
||||||
|
/>
|
||||||
|
<Text style={tertiary}>Verify Your Identity</Text>
|
||||||
|
<Heading style={secondary}>
|
||||||
|
Enter the following code to finish linking Venmo.
|
||||||
|
</Heading>
|
||||||
|
<Section style={codeContainer}>
|
||||||
|
<Text style={code}>{validationCode}</Text>
|
||||||
|
</Section>
|
||||||
|
<Text style={paragraph}>Not expecting this email?</Text>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
Contact{" "}
|
||||||
|
<Link href="mailto:login@plaid.com" style={link}>
|
||||||
|
login@plaid.com
|
||||||
|
</Link>{" "}
|
||||||
|
if you did not request this code.
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
<Text style={footer}>Securely powered by Plaid.</Text>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
|
||||||
|
PlaidVerifyIdentityEmail.PreviewProps = {
|
||||||
|
validationCode: "144833",
|
||||||
|
} as PlaidVerifyIdentityEmailProps;
|
||||||
|
|
||||||
|
export default PlaidVerifyIdentityEmail;
|
||||||
|
|
||||||
|
const main = {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
border: "1px solid #eee",
|
||||||
|
borderRadius: "5px",
|
||||||
|
boxShadow: "0 5px 10px rgba(20,50,70,.2)",
|
||||||
|
marginTop: "20px",
|
||||||
|
maxWidth: "360px",
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: "68px 0 130px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const logo = {
|
||||||
|
margin: "0 auto",
|
||||||
|
};
|
||||||
|
|
||||||
|
const tertiary = {
|
||||||
|
color: "#0a85ea",
|
||||||
|
fontSize: "11px",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
|
||||||
|
height: "16px",
|
||||||
|
letterSpacing: "0",
|
||||||
|
lineHeight: "16px",
|
||||||
|
margin: "16px 8px 8px 8px",
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
textAlign: "center" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondary = {
|
||||||
|
color: "#000",
|
||||||
|
display: "inline-block",
|
||||||
|
fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: "24px",
|
||||||
|
marginBottom: "0",
|
||||||
|
marginTop: "0",
|
||||||
|
textAlign: "center" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const codeContainer = {
|
||||||
|
background: "rgba(0,0,0,.05)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
margin: "16px auto 14px",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
width: "280px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const code = {
|
||||||
|
color: "#000",
|
||||||
|
display: "inline-block",
|
||||||
|
fontFamily: "HelveticaNeue-Bold",
|
||||||
|
fontSize: "32px",
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: "6px",
|
||||||
|
lineHeight: "40px",
|
||||||
|
paddingBottom: "8px",
|
||||||
|
paddingTop: "8px",
|
||||||
|
margin: "0 auto",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const paragraph = {
|
||||||
|
color: "#444",
|
||||||
|
fontSize: "15px",
|
||||||
|
fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
|
||||||
|
letterSpacing: "0",
|
||||||
|
lineHeight: "23px",
|
||||||
|
padding: "0 40px",
|
||||||
|
margin: "0",
|
||||||
|
textAlign: "center" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const link = {
|
||||||
|
color: "#444",
|
||||||
|
textDecoration: "underline",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footer = {
|
||||||
|
color: "#000",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "0",
|
||||||
|
lineHeight: "23px",
|
||||||
|
margin: "0",
|
||||||
|
marginTop: "20px",
|
||||||
|
fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
|
||||||
|
textAlign: "center" as const,
|
||||||
|
textTransform: "uppercase" as const,
|
||||||
|
};
|
||||||
BIN
emails/emails/static/logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
emails/emails/static/notion-logo.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
emails/emails/static/plaid-logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
emails/emails/static/plaid.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
emails/emails/static/stripe-logo.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
emails/emails/static/vercel-arrow.png
Normal file
|
After Width: | Height: | Size: 426 B |
BIN
emails/emails/static/vercel-logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
emails/emails/static/vercel-team.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
emails/emails/static/vercel-user.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
152
emails/emails/stripe-welcome.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Hr,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const baseUrl = process.env.VERCEL_URL
|
||||||
|
? `https://${process.env.VERCEL_URL}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
export const StripeWelcomeEmail = () => (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>You're now ready to make live transactions with Stripe!</Preview>
|
||||||
|
<Body style={main}>
|
||||||
|
<Container style={container}>
|
||||||
|
<Section style={box}>
|
||||||
|
<Img
|
||||||
|
src={`${baseUrl}/static/stripe-logo.png`}
|
||||||
|
width="49"
|
||||||
|
height="21"
|
||||||
|
alt="Stripe"
|
||||||
|
/>
|
||||||
|
<Hr style={hr} />
|
||||||
|
<Text style={paragraph}>
|
||||||
|
Thanks for submitting your account information. You're now ready to
|
||||||
|
make live transactions with Stripe!
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
You can view your payments and a variety of other information about
|
||||||
|
your account right from your dashboard.
|
||||||
|
</Text>
|
||||||
|
<Button style={button} href="https://dashboard.stripe.com/login">
|
||||||
|
View your Stripe Dashboard
|
||||||
|
</Button>
|
||||||
|
<Hr style={hr} />
|
||||||
|
<Text style={paragraph}>
|
||||||
|
If you haven't finished your integration, you might find our{" "}
|
||||||
|
<Link style={anchor} href="https://stripe.com/docs">
|
||||||
|
docs
|
||||||
|
</Link>{" "}
|
||||||
|
handy.
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
Once you're ready to start accepting payments, you'll just need to
|
||||||
|
use your live{" "}
|
||||||
|
<Link
|
||||||
|
style={anchor}
|
||||||
|
href="https://dashboard.stripe.com/login?redirect=%2Fapikeys"
|
||||||
|
>
|
||||||
|
API keys
|
||||||
|
</Link>{" "}
|
||||||
|
instead of your test API keys. Your account can simultaneously be
|
||||||
|
used for both test and live requests, so you can continue testing
|
||||||
|
while accepting live payments. Check out our{" "}
|
||||||
|
<Link style={anchor} href="https://stripe.com/docs/dashboard">
|
||||||
|
tutorial about account basics
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
Finally, we've put together a{" "}
|
||||||
|
<Link
|
||||||
|
style={anchor}
|
||||||
|
href="https://stripe.com/docs/checklist/website"
|
||||||
|
>
|
||||||
|
quick checklist
|
||||||
|
</Link>{" "}
|
||||||
|
to ensure your website conforms to card network standards.
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
We'll be here to help you with any step along the way. You can find
|
||||||
|
answers to most questions and get in touch with us on our{" "}
|
||||||
|
<Link style={anchor} href="https://support.stripe.com/">
|
||||||
|
support site
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraph}>— The Stripe team</Text>
|
||||||
|
<Hr style={hr} />
|
||||||
|
<Text style={footer}>
|
||||||
|
Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default StripeWelcomeEmail;
|
||||||
|
|
||||||
|
const main = {
|
||||||
|
backgroundColor: "#f6f9fc",
|
||||||
|
fontFamily:
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: "20px 0 48px",
|
||||||
|
marginBottom: "64px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const box = {
|
||||||
|
padding: "0 48px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const hr = {
|
||||||
|
borderColor: "#e6ebf1",
|
||||||
|
margin: "20px 0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const paragraph = {
|
||||||
|
color: "#525f7f",
|
||||||
|
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
textAlign: "left" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const anchor = {
|
||||||
|
color: "#556cd6",
|
||||||
|
};
|
||||||
|
|
||||||
|
const button = {
|
||||||
|
backgroundColor: "#656ee8",
|
||||||
|
borderRadius: "5px",
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textDecoration: "none",
|
||||||
|
textAlign: "center" as const,
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
padding: "10px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const footer = {
|
||||||
|
color: "#8898aa",
|
||||||
|
fontSize: "12px",
|
||||||
|
lineHeight: "16px",
|
||||||
|
};
|
||||||
154
emails/emails/vercel-invite-user.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Column,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Hr,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Row,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface VercelInviteUserEmailProps {
|
||||||
|
username?: string;
|
||||||
|
userImage?: string;
|
||||||
|
invitedByUsername?: string;
|
||||||
|
invitedByEmail?: string;
|
||||||
|
teamName?: string;
|
||||||
|
teamImage?: string;
|
||||||
|
inviteLink?: string;
|
||||||
|
inviteFromIp?: string;
|
||||||
|
inviteFromLocation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = process.env.VERCEL_URL
|
||||||
|
? `https://${process.env.VERCEL_URL}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
export const VercelInviteUserEmail = ({
|
||||||
|
username,
|
||||||
|
userImage,
|
||||||
|
invitedByUsername,
|
||||||
|
invitedByEmail,
|
||||||
|
teamName,
|
||||||
|
teamImage,
|
||||||
|
inviteLink,
|
||||||
|
inviteFromIp,
|
||||||
|
inviteFromLocation,
|
||||||
|
}: VercelInviteUserEmailProps) => {
|
||||||
|
const previewText = `Join ${invitedByUsername} on Vercel`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind>
|
||||||
|
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||||
|
<Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||||
|
<Section className="mt-[32px]">
|
||||||
|
<Img
|
||||||
|
src={`${baseUrl}/static/vercel-logo.png`}
|
||||||
|
width="40"
|
||||||
|
height="37"
|
||||||
|
alt="Vercel"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||||
|
Join <strong>{teamName}</strong> on <strong>Vercel</strong>
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
Hello {username},
|
||||||
|
</Text>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
<strong>{invitedByUsername}</strong> (
|
||||||
|
<Link
|
||||||
|
href={`mailto:${invitedByEmail}`}
|
||||||
|
className="text-blue-600 no-underline"
|
||||||
|
>
|
||||||
|
{invitedByEmail}
|
||||||
|
</Link>
|
||||||
|
) has invited you to the <strong>{teamName}</strong> team on{" "}
|
||||||
|
<strong>Vercel</strong>.
|
||||||
|
</Text>
|
||||||
|
<Section>
|
||||||
|
<Row>
|
||||||
|
<Column align="right">
|
||||||
|
<Img
|
||||||
|
className="rounded-full"
|
||||||
|
src={userImage}
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
<Column align="center">
|
||||||
|
<Img
|
||||||
|
src={`${baseUrl}/static/vercel-arrow.png`}
|
||||||
|
width="12"
|
||||||
|
height="9"
|
||||||
|
alt="invited you to"
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
<Column align="left">
|
||||||
|
<Img
|
||||||
|
className="rounded-full"
|
||||||
|
src={teamImage}
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
<Section className="text-center mt-[32px] mb-[32px]">
|
||||||
|
<Button
|
||||||
|
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
|
||||||
|
href={inviteLink}
|
||||||
|
>
|
||||||
|
Join the team
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
|
or copy and paste this URL into your browser:{" "}
|
||||||
|
<Link href={inviteLink} className="text-blue-600 no-underline">
|
||||||
|
{inviteLink}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
|
||||||
|
<Text className="text-[#666666] text-[12px] leading-[24px]">
|
||||||
|
This invitation was intended for{" "}
|
||||||
|
<span className="text-black">{username}</span>. This invite was
|
||||||
|
sent from <span className="text-black">{inviteFromIp}</span>{" "}
|
||||||
|
located in{" "}
|
||||||
|
<span className="text-black">{inviteFromLocation}</span>. If you
|
||||||
|
were not expecting this invitation, you can ignore this email. If
|
||||||
|
you are concerned about your account's safety, please reply to
|
||||||
|
this email to get in touch with us.
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
VercelInviteUserEmail.PreviewProps = {
|
||||||
|
username: "alanturing",
|
||||||
|
userImage: `${baseUrl}/static/vercel-user.png`,
|
||||||
|
invitedByUsername: "Alan",
|
||||||
|
invitedByEmail: "alan.turing@example.com",
|
||||||
|
teamName: "Enigma",
|
||||||
|
teamImage: `${baseUrl}/static/vercel-team.png`,
|
||||||
|
inviteLink: "https://vercel.com/teams/invite/foo",
|
||||||
|
inviteFromIp: "204.13.186.218",
|
||||||
|
inviteFromLocation: "São Paulo, Brazil",
|
||||||
|
} as VercelInviteUserEmailProps;
|
||||||
|
|
||||||
|
export default VercelInviteUserEmail;
|
||||||
20
emails/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "emails",
|
||||||
|
"version": "0.0.19",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "email build",
|
||||||
|
"dev": "email dev",
|
||||||
|
"export": "email export"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-email/components": "0.0.21",
|
||||||
|
"react-email": "2.1.5",
|
||||||
|
"react": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "18.2.33",
|
||||||
|
"@types/react-dom": "18.2.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
4209
emails/pnpm-lock.yaml
generated
Normal file
27
emails/readme.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# React Email Starter
|
||||||
|
|
||||||
|
A live preview right in your browser so you don't need to keep sending real emails during development.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, install the dependencies:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
# or
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run the development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
24
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.3.3",
|
"version": "v0.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
"db:push": "drizzle-kit --config ./server/db/drizzle.config.ts",
|
"db:push": "drizzle-kit --config ./server/db/drizzle.config.ts",
|
||||||
"db:truncate": "tsx -r dotenv/config ./server/db/reset.ts",
|
"db:truncate": "tsx -r dotenv/config ./server/db/reset.ts",
|
||||||
"db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"db:studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"check": "biome check",
|
"check": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true",
|
||||||
"format": "biome format",
|
"format": "biome format --write",
|
||||||
"lint": "biome lint",
|
"lint": "biome lint",
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc",
|
||||||
"db:seed": "tsx -r dotenv/config ./server/db/seed.ts",
|
"db:seed": "tsx -r dotenv/config ./server/db/seed.ts",
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@react-email/components": "^0.0.21",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@tanstack/react-table": "^8.16.0",
|
"@tanstack/react-table": "^8.16.0",
|
||||||
"@trpc/client": "^10.43.6",
|
"@trpc/client": "^10.43.6",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"@uiw/react-codemirror": "^4.22.1",
|
"@uiw/react-codemirror": "^4.22.1",
|
||||||
"@xterm/addon-attach": "0.10.0",
|
"@xterm/addon-attach": "0.10.0",
|
||||||
"@xterm/xterm": "^5.4.0",
|
"@xterm/xterm": "^5.4.0",
|
||||||
|
"adm-zip": "^0.5.14",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
@@ -103,6 +105,7 @@
|
|||||||
"node-os-utils": "1.3.7",
|
"node-os-utils": "1.3.7",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
|
"nodemailer": "6.9.14",
|
||||||
"octokit": "3.1.2",
|
"octokit": "3.1.2",
|
||||||
"otpauth": "^9.2.3",
|
"otpauth": "^9.2.3",
|
||||||
"postgres": "3.4.4",
|
"postgres": "3.4.4",
|
||||||
@@ -119,13 +122,18 @@
|
|||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tar-fs": "3.0.5",
|
"tar-fs": "3.0.5",
|
||||||
|
"undici": "^6.19.2",
|
||||||
"use-resize-observer": "9.1.0",
|
"use-resize-observer": "9.1.0",
|
||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4"
|
"zod": "^3.23.4",
|
||||||
|
"zod-form-data": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.8.3",
|
||||||
|
"@commitlint/cli": "^19.3.0",
|
||||||
|
"@commitlint/config-conventional": "^19.2.2",
|
||||||
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/dockerode": "3.3.23",
|
"@types/dockerode": "3.3.23",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
@@ -133,6 +141,7 @@
|
|||||||
"@types/node": "^18.17.0",
|
"@types/node": "^18.17.0",
|
||||||
"@types/node-os-utils": "1.3.4",
|
"@types/node-os-utils": "1.3.4",
|
||||||
"@types/node-schedule": "2.1.6",
|
"@types/node-schedule": "2.1.6",
|
||||||
|
"@types/nodemailer": "^6.4.15",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
@@ -158,14 +167,17 @@
|
|||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.25.2"
|
"initVersion": "7.25.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.15.4",
|
"packageManager": "pnpm@9.5.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0",
|
"node": "^18.18.0",
|
||||||
"pnpm": ">=8.15.4"
|
"pnpm": ">=9.5.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": [
|
"*": [
|
||||||
"biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
|
"biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"commitlint": {
|
||||||
|
"extends": ["@commitlint/config-conventional"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.status(401).json({ message: "Unauthorized" });
|
res.status(401).json({ message: "Unauthorized" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return createOpenApiNextHandler({
|
return createOpenApiNextHandler({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export default async function handler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!application?.autoDeploy) {
|
if (!application?.autoDeploy) {
|
||||||
res.status(400).json({ message: "Application Not Deployable" });
|
res.status(400).json({
|
||||||
|
message: "Automatic deployments are disabled for this application",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export default async function handler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!composeResult?.autoDeploy) {
|
if (!composeResult?.autoDeploy) {
|
||||||
res.status(400).json({ message: "Compose Not Deployable" });
|
res.status(400).json({
|
||||||
|
message: "Automatic deployments are disabled for this compose",
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
|
||||||
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { createTRPCContext } from "@/server/api/trpc";
|
import { createTRPCContext } from "@/server/api/trpc";
|
||||||
|
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||||
|
import { nodeHTTPFormDataContentTypeHandler } from "@trpc/server/adapters/node-http/content-type/form-data";
|
||||||
|
import { nodeHTTPJSONContentTypeHandler } from "@trpc/server/adapters/node-http/content-type/json";
|
||||||
|
|
||||||
// export API handler
|
// export API handler
|
||||||
export default createNextApiHandler({
|
export default createNextApiHandler({
|
||||||
@@ -15,4 +16,15 @@ export default createNextApiHandler({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
experimental_contentTypeHandlers: [
|
||||||
|
nodeHTTPFormDataContentTypeHandler(),
|
||||||
|
nodeHTTPJSONContentTypeHandler(),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
sizeLimit: "1gb",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
42
pages/dashboard/settings/notifications.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||||
|
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { validateRequest } from "@/server/auth/auth";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import React, { type ReactElement } from "react";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<ShowNotifications />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
Page.getLayout = (page: ReactElement) => {
|
||||||
|
return (
|
||||||
|
<DashboardLayout tab={"settings"}>
|
||||||
|
<SettingsLayout>{page}</SettingsLayout>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(
|
||||||
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
) {
|
||||||
|
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||||
|
if (!user || user.rol === "user") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
15291
pnpm-lock.yaml
generated
@@ -14,6 +14,7 @@ import { mariadbRouter } from "./routers/mariadb";
|
|||||||
import { mongoRouter } from "./routers/mongo";
|
import { mongoRouter } from "./routers/mongo";
|
||||||
import { mountRouter } from "./routers/mount";
|
import { mountRouter } from "./routers/mount";
|
||||||
import { mysqlRouter } from "./routers/mysql";
|
import { mysqlRouter } from "./routers/mysql";
|
||||||
|
import { notificationRouter } from "./routers/notification";
|
||||||
import { portRouter } from "./routers/port";
|
import { portRouter } from "./routers/port";
|
||||||
import { postgresRouter } from "./routers/postgres";
|
import { postgresRouter } from "./routers/postgres";
|
||||||
import { projectRouter } from "./routers/project";
|
import { projectRouter } from "./routers/project";
|
||||||
@@ -54,6 +55,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
port: portRouter,
|
port: portRouter,
|
||||||
registry: registryRouter,
|
registry: registryRouter,
|
||||||
cluster: clusterRouter,
|
cluster: clusterRouter,
|
||||||
|
notification: notificationRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
uploadProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
apiCreateApplication,
|
apiCreateApplication,
|
||||||
@@ -52,6 +56,9 @@ import {
|
|||||||
import { removeDeployments } from "../services/deployment";
|
import { removeDeployments } from "../services/deployment";
|
||||||
import { addNewService, checkServiceAccess } from "../services/user";
|
import { addNewService, checkServiceAccess } from "../services/user";
|
||||||
|
|
||||||
|
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||||
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
|
|
||||||
export const applicationRouter = createTRPCRouter({
|
export const applicationRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
@@ -324,6 +331,45 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
return traefikConfig;
|
return traefikConfig;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
dropDeployment: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
path: "/drop-deployment",
|
||||||
|
method: "POST",
|
||||||
|
override: true,
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.use(uploadProcedure)
|
||||||
|
.input(uploadFileSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const zipFile = input.zip;
|
||||||
|
|
||||||
|
updateApplication(input.applicationId as string, {
|
||||||
|
sourceType: "drop",
|
||||||
|
dropBuildPath: input.dropBuildPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = await findApplicationById(input.applicationId as string);
|
||||||
|
await unzipDrop(zipFile, app.appName);
|
||||||
|
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
applicationId: app.applicationId,
|
||||||
|
titleLog: "Manual deployment",
|
||||||
|
descriptionLog: "",
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "application",
|
||||||
|
};
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
updateTraefikConfig: protectedProcedure
|
updateTraefikConfig: protectedProcedure
|
||||||
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
.input(z.object({ applicationId: z.string(), traefikConfig: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||||
await runPostgresBackup(postgres, backup);
|
await runPostgresBackup(postgres, backup);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from "@/templates/utils";
|
} from "@/templates/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import _ from "lodash";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { findAdmin } from "../services/admin";
|
import { findAdmin } from "../services/admin";
|
||||||
import {
|
import {
|
||||||
@@ -283,4 +284,10 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return templatesData;
|
return templatesData;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getTags: protectedProcedure.query(async ({ input }) => {
|
||||||
|
const allTags = templates.flatMap((template) => template.tags);
|
||||||
|
const uniqueTags = _.uniq(allTags);
|
||||||
|
return uniqueTags;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
255
server/api/routers/notification.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import {
|
||||||
|
adminProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import {
|
||||||
|
apiCreateDiscord,
|
||||||
|
apiCreateEmail,
|
||||||
|
apiCreateSlack,
|
||||||
|
apiCreateTelegram,
|
||||||
|
apiFindOneNotification,
|
||||||
|
apiTestDiscordConnection,
|
||||||
|
apiTestEmailConnection,
|
||||||
|
apiTestSlackConnection,
|
||||||
|
apiTestTelegramConnection,
|
||||||
|
apiUpdateDiscord,
|
||||||
|
apiUpdateEmail,
|
||||||
|
apiUpdateSlack,
|
||||||
|
apiUpdateTelegram,
|
||||||
|
notifications,
|
||||||
|
} from "@/server/db/schema";
|
||||||
|
import {
|
||||||
|
sendDiscordNotification,
|
||||||
|
sendEmailNotification,
|
||||||
|
sendSlackNotification,
|
||||||
|
sendTelegramNotification,
|
||||||
|
} from "@/server/utils/notifications/utils";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { desc } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
createDiscordNotification,
|
||||||
|
createEmailNotification,
|
||||||
|
createSlackNotification,
|
||||||
|
createTelegramNotification,
|
||||||
|
findNotificationById,
|
||||||
|
removeNotificationById,
|
||||||
|
updateDiscordNotification,
|
||||||
|
updateEmailNotification,
|
||||||
|
updateSlackNotification,
|
||||||
|
updateTelegramNotification,
|
||||||
|
} from "../services/notification";
|
||||||
|
|
||||||
|
export const notificationRouter = createTRPCRouter({
|
||||||
|
createSlack: adminProcedure
|
||||||
|
.input(apiCreateSlack)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await createSlackNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
updateSlack: adminProcedure
|
||||||
|
.input(apiUpdateSlack)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await updateSlackNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to update the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
testSlackConnection: adminProcedure
|
||||||
|
.input(apiTestSlackConnection)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await sendSlackNotification(input, {
|
||||||
|
channel: input.channel,
|
||||||
|
text: "Hi, From Dokploy 👋",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to test the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createTelegram: adminProcedure
|
||||||
|
.input(apiCreateTelegram)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await createTelegramNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateTelegram: adminProcedure
|
||||||
|
.input(apiUpdateTelegram)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await updateTelegramNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to update the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
testTelegramConnection: adminProcedure
|
||||||
|
.input(apiTestTelegramConnection)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await sendTelegramNotification(input, "Hi, From Dokploy 👋");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to test the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createDiscord: adminProcedure
|
||||||
|
.input(apiCreateDiscord)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
// go to your discord server
|
||||||
|
// go to settings
|
||||||
|
// go to integrations
|
||||||
|
// add a new integration
|
||||||
|
// select webhook
|
||||||
|
// copy the webhook url
|
||||||
|
return await createDiscordNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateDiscord: adminProcedure
|
||||||
|
.input(apiUpdateDiscord)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await updateDiscordNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to update the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
testDiscordConnection: adminProcedure
|
||||||
|
.input(apiTestDiscordConnection)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await sendDiscordNotification(input, {
|
||||||
|
title: "Test Notification",
|
||||||
|
description: "Hi, From Dokploy 👋",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to test the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
createEmail: adminProcedure
|
||||||
|
.input(apiCreateEmail)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await createEmailNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to create the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
updateEmail: adminProcedure
|
||||||
|
.input(apiUpdateEmail)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await updateEmailNotification(input);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to update the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
testEmailConnection: adminProcedure
|
||||||
|
.input(apiTestEmailConnection)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await sendEmailNotification(
|
||||||
|
input,
|
||||||
|
"Test Email",
|
||||||
|
"<p>Hi, From Dokploy 👋</p>",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to test the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
remove: adminProcedure
|
||||||
|
.input(apiFindOneNotification)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
return await removeNotificationById(input.notificationId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error to delete this notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
one: protectedProcedure
|
||||||
|
.input(apiFindOneNotification)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const notification = await findNotificationById(input.notificationId);
|
||||||
|
return notification;
|
||||||
|
}),
|
||||||
|
all: adminProcedure.query(async () => {
|
||||||
|
return await db.query.notifications.findMany({
|
||||||
|
with: {
|
||||||
|
slack: true,
|
||||||
|
telegram: true,
|
||||||
|
discord: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
orderBy: desc(notifications.createdAt),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||