Merge branch 'canary' into feat/template-evolution-api

This commit is contained in:
Mauricio Siu
2025-01-30 23:16:09 -06:00
140 changed files with 25621 additions and 1067 deletions

View File

@@ -1,119 +0,0 @@
version: 2.1
jobs:
build-amd64:
machine:
image: ubuntu-2004:current
steps:
- checkout
- run:
name: Prepare .env file
command: |
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.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"
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
else
TAG="feature"
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 apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.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"
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
else
TAG="feature"
fi
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
docker push dokploy/dokploy:${TAG}-arm64
combine-manifests:
docker:
- image: cimg/node:20.9.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('./apps/dokploy/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}
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
else
TAG="feature"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
fi
workflows:
build-all:
jobs:
- build-amd64:
filters:
branches:
only:
- main
- canary
- feat/add-sidebar
- build-arm64:
filters:
branches:
only:
- main
- canary
- feat/add-sidebar
- combine-manifests:
requires:
- build-amd64
- build-arm64
filters:
branches:
only:
- main
- canary
- feat/add-sidebar

83
.github/workflows/create-pr.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
name: Auto PR to main when version changes
on:
push:
branches:
- canary
permissions:
contents: write
pull-requests: write
jobs:
create-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version from package.json
id: package_version
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
- name: Get latest GitHub tag
id: latest_tag
run: |
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
echo $LATEST_TAG
- name: Compare versions
id: compare_versions
run: |
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
VERSION_CHANGED="true"
else
VERSION_CHANGED="false"
fi
echo "VERSION_CHANGED=$VERSION_CHANGED" >> $GITHUB_ENV
echo "Comparing versions:"
echo "Current version: ${{ env.VERSION }}"
echo "Latest tag: ${{ env.LATEST_TAG }}"
echo "Version changed: $VERSION_CHANGED"
- name: Check if a PR already exists
id: check_pr
run: |
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
- name: Create Pull Request
if: env.VERSION_CHANGED == 'true' && env.PR_EXISTS == '0'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin main
git checkout canary
git push origin canary
gh pr create \
--title "🚀 Release ${{ env.VERSION }}" \
--body '
This PR promotes changes from `canary` to `main` for version ${{ env.VERSION }}.
### 🔍 Changes Include:
- Version bump to ${{ env.VERSION }}
- All changes from canary branch
### ✅ Pre-merge Checklist:
- [ ] All tests passing
- [ ] Documentation updated
- [ ] Docker images built and tested
> 🤖 This PR was automatically generated by [GitHub Actions](https://github.com/actions)' \
--base main \
--head canary \
--label "release" --label "automated pr" || true \
--reviewer siumauricio \
--assignee siumauricio
env:
GH_TOKEN: ${{ github.token }}

161
.github/workflows/dokploy.yml vendored Normal file
View File

@@ -0,0 +1,161 @@
name: Dokploy Docker Build
on:
push:
branches: [main, canary, feat/github-runners]
env:
IMAGE_NAME: dokploy/dokploy
jobs:
docker-amd:
runs-on: ubuntu-22.04
steps:
- name: Checkout
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: Set tag and version
id: meta
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
else
TAG="feature"
fi
echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT
- name: Prepare env file
run: |
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
docker-arm:
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
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: Set tag and version
id: meta
run: |
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
TAG="latest"
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
else
TAG="feature"
fi
echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT
- name: Prepare env file
run: |
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
combine-manifests:
needs: [docker-amd, docker-arm]
runs-on: ubuntu-latest
steps:
- name: Checkout
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: Create and push manifests
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
TAG="latest"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
docker buildx imagetools create -t ${IMAGE_NAME}:${VERSION} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then
TAG="canary"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
else
TAG="feature"
docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \
${IMAGE_NAME}:${TAG}-amd64 \
${IMAGE_NAME}:${TAG}-arm64
fi
generate-release:
needs: [combine-manifests]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get version
id: get_version
run: |
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.get_version.outputs.version }}
name: ${{ steps.get_version.outputs.version }}
generate_release_notes: true
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,98 @@
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import { describe, expect, it } from "vitest";
describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = {
"x-github-event": "push",
};
const createMockBody = (message: string) => ({
head_commit: {
message,
},
});
const skipKeywords = [
"[skip ci]",
"[ci skip]",
"[no ci]",
"[skip actions]",
"[actions skip]",
];
it("should detect skip keywords in commit message", () => {
for (const keyword of skipKeywords) {
const message = `feat: add new feature ${keyword}`;
const commitMessage = extractCommitMessage(
mockGithubHeaders,
createMockBody(message),
);
expect(commitMessage.includes(keyword)).toBe(true);
}
});
it("should not detect skip keywords in normal commit message", () => {
const message = "feat: add new feature";
const commitMessage = extractCommitMessage(
mockGithubHeaders,
createMockBody(message),
);
for (const keyword of skipKeywords) {
expect(commitMessage.includes(keyword)).toBe(false);
}
});
it("should handle different webhook sources", () => {
// GitHub
expect(
extractCommitMessage(
{ "x-github-event": "push" },
{ head_commit: { message: "[skip ci] test" } },
),
).toBe("[skip ci] test");
// GitLab
expect(
extractCommitMessage(
{ "x-gitlab-event": "push" },
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
// Bitbucket
expect(
extractCommitMessage(
{ "x-event-key": "repo:push" },
{
push: {
changes: [{ new: { target: { message: "[skip ci] test" } } }],
},
},
),
).toBe("[skip ci] test");
// Gitea
expect(
extractCommitMessage(
{ "x-gitea-event": "push" },
{ commits: [{ message: "[skip ci] test" }] },
),
).toBe("[skip ci] test");
});
it("should handle missing commit message", () => {
expect(extractCommitMessage(mockGithubHeaders, {})).toBe("NEW COMMIT");
expect(extractCommitMessage({ "x-gitlab-event": "push" }, {})).toBe(
"NEW COMMIT",
);
expect(
extractCommitMessage(
{ "x-event-key": "repo:push" },
{ push: { changes: [] } },
),
).toBe("NEW COMMIT");
expect(extractCommitMessage({ "x-gitea-event": "push" }, {})).toBe(
"NEW COMMIT",
);
});
});

View File

@@ -14,6 +14,9 @@ import {
import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = {
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
createdAt: "",
authId: "",
adminId: "string",

View File

@@ -13,6 +13,7 @@ export default defineConfig({
NODE: "test",
},
},
plugins: [tsconfigPaths()],
resolve: {
alias: {
"@dokploy/server": path.resolve(

View File

@@ -144,38 +144,6 @@ export const ShowResources = ({ id, type }: Props) => {
className="grid w-full gap-8 "
>
<div className="grid w-full md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryLimit"
@@ -209,6 +177,37 @@ export const ShowResources = ({ id, type }: Props) => {
);
}}
/>
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="268435456 (256MB in bytes)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}

View File

@@ -45,8 +45,8 @@ export const ShowVolumes = ({ id, type }: Props) => {
<div>
<CardTitle className="text-xl">Volumes</CardTitle>
<CardDescription>
If you want to persist data in this postgres database use the
following config to setup the volumes
If you want to persist data in this service use the following config
to setup the volumes
</CardDescription>
</div>
@@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
{mount.type === "file" && (
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
{mount.content}
</span>
</div>
@@ -113,12 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</span>
</div>
)}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
{mount.type === "file" ? (
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</span>
</div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)}
</div>
<div className="flex flex-row gap-1">
<UpdateVolume

View File

@@ -17,8 +17,15 @@ interface Props {
open: boolean;
onClose: () => void;
serverId?: string;
errorMessage?: string;
}
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
export const ShowDeployment = ({
logPath,
open,
onClose,
serverId,
errorMessage,
}: Props) => {
const [data, setData] = useState("");
const [showExtraLogs, setShowExtraLogs] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
@@ -99,6 +106,8 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
}
}, [filteredLogs, autoScroll]);
const optionalErrors = parseLogs(errorMessage || "");
return (
<Dialog
open={open}
@@ -157,9 +166,17 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</>
)}
</div>
</DialogContent>

View File

@@ -8,7 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues";
@@ -18,8 +18,11 @@ import { ShowDeployment } from "./show-deployment";
interface Props {
applicationId: string;
}
export const ShowDeployments = ({ applicationId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const { data } = api.application.one.useQuery({ applicationId });
const { data: deployments } = api.deployment.all.useQuery(
{ applicationId },
@@ -100,7 +103,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -112,9 +115,10 @@ export const ShowDeployments = ({ applicationId }: Props) => {
)}
<ShowDeployment
serverId={data?.serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</CardContent>
</Card>

View File

@@ -98,8 +98,12 @@ export const ShowDomains = ({ applicationId }: Props) => {
applicationId={applicationId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</AddDomain>
<DialogAction

View File

@@ -235,7 +235,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -245,7 +245,12 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -226,7 +226,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -236,7 +236,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -248,7 +248,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -260,7 +260,12 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -26,7 +26,9 @@ export const ShowPreviewBuilds = ({
serverId,
trigger,
}: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -77,7 +79,7 @@ export const ShowPreviewBuilds = ({
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -89,9 +91,10 @@ export const ShowPreviewBuilds = ({
</DialogContent>
<ShowDeployment
serverId={serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</Dialog>
);

View File

@@ -20,9 +20,10 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import type { ServiceType } from "@dokploy/server/db/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -39,16 +40,42 @@ const deleteComposeSchema = z.object({
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
interface Props {
composeId: string;
id: string;
type: ServiceType | "application";
}
export const DeleteCompose = ({ composeId }: Props) => {
export const DeleteService = ({ id, type }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
const { data } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
compose: () =>
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const mutationMap = {
postgres: () => api.postgres.remove.useMutation(),
redis: () => api.redis.remove.useMutation(),
mysql: () => api.mysql.remove.useMutation(),
mariadb: () => api.mariadb.remove.useMutation(),
application: () => api.application.delete.useMutation(),
mongo: () => api.mongo.remove.useMutation(),
compose: () => api.compose.delete.useMutation(),
};
const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.remove.useMutation();
const { push } = useRouter();
const form = useForm<DeleteCompose>({
defaultValues: {
@@ -62,14 +89,23 @@ export const DeleteCompose = ({ composeId }: Props) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
const { deleteVolumes } = formData;
await mutateAsync({ composeId, deleteVolumes })
await mutateAsync({
mongoId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
applicationId: id || "",
composeId: id || "",
deleteVolumes,
})
.then((result) => {
push(`/dashboard/project/${result?.projectId}`);
toast.success("Compose deleted successfully");
toast.success("deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the compose");
toast.error("Error deleting the service");
});
} else {
form.setError("projectName", {
@@ -95,8 +131,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
compose. If you are sure please enter the compose name to delete
this compose.
service. If you are sure please enter the service name to delete
this service.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
@@ -119,9 +155,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
variant="outline"
onClick={() => {
if (data?.name && data?.appName) {
navigator.clipboard.writeText(
`${data.name}/${data.appName}`,
);
copy(`${data.name}/${data.appName}`);
toast.success("Copied to clipboard. Be careful!");
}
}}
@@ -142,27 +176,29 @@ export const DeleteCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
{type === "compose" && (
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
</div>

View File

@@ -17,12 +17,14 @@ interface Props {
serverId?: string;
open: boolean;
onClose: () => void;
errorMessage?: string;
}
export const ShowDeploymentCompose = ({
logPath,
open,
onClose,
serverId,
errorMessage,
}: Props) => {
const [data, setData] = useState("");
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
@@ -105,6 +107,8 @@ export const ShowDeploymentCompose = ({
}
}, [filteredLogs, autoScroll]);
const optionalErrors = parseLogs(errorMessage || "");
return (
<Dialog
open={open}
@@ -161,9 +165,17 @@ export const ShowDeploymentCompose = ({
<TerminalLine key={index} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
<>
{optionalErrors.length > 0 ? (
optionalErrors.map((log: LogLine, index: number) => (
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
))
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
)}
</>
)}
</div>
</DialogContent>

View File

@@ -8,7 +8,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueuesCompose } from "./cancel-queues-compose";
@@ -19,7 +19,9 @@ interface Props {
composeId: string;
}
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const { data } = api.compose.one.useQuery({ composeId });
const { data: deployments } = api.deployment.allByCompose.useQuery(
{ composeId },
@@ -100,7 +102,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
<Button
onClick={() => {
setActiveLog(deployment.logPath);
setActiveLog(deployment);
}}
>
View
@@ -112,9 +114,10 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
)}
<ShowDeploymentCompose
serverId={data?.serverId || ""}
open={activeLog !== null}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</CardContent>
</Card>

View File

@@ -97,8 +97,12 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
composeId={composeId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
<Button
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10 "
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</Button>
</AddDomainCompose>
<DialogAction

View File

@@ -237,7 +237,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -247,7 +247,12 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -228,7 +228,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -238,7 +238,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.login}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -250,7 +250,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
value={repo.name}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
@@ -262,7 +262,12 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
form.setValue("branch", "");
}}
>
{repo.name}
<span className="flex items-center gap-2">
<span>{repo.name}</span>
<span className="text-muted-foreground text-xs">
{repo.owner.username}
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",

View File

@@ -75,14 +75,14 @@ export const ShowBackups = ({ id, type }: Props) => {
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
<span className="text-base text-muted-foreground text-center">
To create a backup it is required to set at least 1 provider.
Please, go to{" "}
<Link
href="/dashboard/settings/server"
href="/dashboard/settings/destinations"
className="text-foreground"
>
Settings
S3 Destinations
</Link>{" "}
to do so.
</span>

View File

@@ -43,7 +43,7 @@ const LOG_STYLES: Record<LogType, LogStyle> = {
export function parseLogs(logString: string): LogLine[] {
// Regex to match the log line format
// Exemple of return :
// Example of return :
// 1 2024-12-10T10:00:00.000Z The server is running on port 8080
// Should return :
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),

View File

@@ -200,7 +200,7 @@ export const DockerMonitoring = ({
</div>
</header>
<div className="grid gap-6 md:grid-cols-2">
<div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>

View File

@@ -70,7 +70,7 @@ interface Props {
export const AddApplication = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
@@ -166,7 +166,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -197,7 +197,12 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -73,6 +73,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation();
@@ -173,7 +174,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -204,7 +205,12 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -89,7 +89,7 @@ const mySchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("postgres"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("postgres"),
databaseUser: z.string().default("postgres"),
})
.merge(baseDatabaseSchema),
@@ -110,7 +110,7 @@ const mySchema = z.discriminatedUnion("type", [
type: z.literal("mysql"),
databaseRootPassword: z.string().default(""),
databaseUser: z.string().default("mysql"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("mysql"),
})
.merge(baseDatabaseSchema),
z
@@ -119,7 +119,7 @@ const mySchema = z.discriminatedUnion("type", [
dockerImage: z.string().default("mariadb:4"),
databaseRootPassword: z.string().default(""),
databaseUser: z.string().default("mariadb"),
databaseName: z.string().min(1, "Database name required"),
databaseName: z.string().default("mariadb"),
})
.merge(baseDatabaseSchema),
]);
@@ -206,7 +206,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
promise = postgresMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "postgres",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
@@ -233,7 +233,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
...commonParams,
databasePassword: data.databasePassword,
databaseRootPassword: data.databaseRootPassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "mariadb",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
@@ -242,7 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
promise = mysqlMutation.mutateAsync({
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseName: data.databaseName || "mysql",
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
databaseRootPassword: data.databaseRootPassword,

View File

@@ -80,6 +80,7 @@ export const AddTemplate = ({ projectId }: Props) => {
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { data } = api.compose.templates.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: tags, isLoading: isLoadingTags } =
api.compose.getTags.useQuery();
@@ -114,26 +115,28 @@ export const AddTemplate = ({ projectId }: Props) => {
</DialogTrigger>
<DialogContent className="max-h-screen sm:max-w-[90vw] p-0">
<DialogHeader className="sticky top-0 z-10 bg-background p-6 border-b">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col space-y-6">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
<div>
<DialogTitle>Create from Template</DialogTitle>
<DialogDescription>
Create an open source application from a template
</DialogDescription>
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<Input
placeholder="Search Template"
onChange={(e) => setQuery(e.target.value)}
className="w-[200px]"
className="w-full sm:w-[200px]"
value={query}
/>
<Popover modal={true}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("w-[200px] justify-between !bg-input")}
className={cn(
"w-full sm:w-[200px] justify-between !bg-input",
)}
>
{isLoadingTags
? "Loading...."
@@ -224,7 +227,11 @@ export const AddTemplate = ({ projectId }: Props) => {
<ScrollArea className="h-[calc(98vh-8rem)]">
<div className="p-6">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isError && (
<AlertBlock type="error" className="mb-4">
{error?.message}
</AlertBlock>
)}
{templates.length === 0 ? (
<div className="flex justify-center items-center w-full gap-2 min-h-[50vh]">
@@ -238,8 +245,8 @@ export const AddTemplate = ({ projectId }: Props) => {
className={cn(
"grid gap-6",
viewMode === "detailed"
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-6"
: "grid-cols-2 sm:grid-cols-4 lg:grid-cols-8",
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6",
)}
>
{templates?.map((template, index) => (
@@ -302,7 +309,7 @@ export const AddTemplate = ({ projectId }: Props) => {
{/* Create Button */}
<div
className={cn(
"flex-none px-6 pb-6 pt-3 mt-auto",
"flex-none px-6 py-3 mt-auto",
viewMode === "detailed"
? "flex items-center justify-between bg-muted/30 border-t"
: "flex justify-center",
@@ -340,6 +347,7 @@ export const AddTemplate = ({ projectId }: Props) => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
size="sm"
className={cn(
"w-auto",
@@ -365,7 +373,8 @@ export const AddTemplate = ({ projectId }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
Select a Server (Optional)
Select a Server{" "}
{!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
@@ -398,7 +407,12 @@ export const AddTemplate = ({ projectId }: Props) => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>

View File

@@ -118,7 +118,7 @@ export const HandleProject = ({ projectId }: Props) => {
</DialogTrigger>
<DialogContent className="sm:m:max-w-lg ">
<DialogHeader>
<DialogTitle>Add a project</DialogTitle>
<DialogTitle>{projectId ? "Update" : "Add a"} project</DialogTitle>
<DialogDescription>The home of something big!</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}

View File

@@ -23,8 +23,10 @@ import {
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
@@ -77,8 +79,8 @@ export const ShowProjects = () => {
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center">
<CardHeader className="">
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className="p-0">
<CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" />
Projects
@@ -87,9 +89,12 @@ export const ShowProjects = () => {
Create and manage your projects
</CardDescription>
</CardHeader>
<div className=" px-4 ">
<HandleProject />
</div>
{(auth?.rol === "admin" || user?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>
)}
</div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
@@ -146,14 +151,91 @@ export const ShowProjects = () => {
href={`/dashboard/project/${project.projectId}`}
>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
{project.applications.length > 0 ||
project.compose.length > 0 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
size="sm"
variant="default"
>
<ExternalLinkIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
{project.applications.length > 0 && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Applications
</DropdownMenuLabel>
{project.applications.map((app) => (
<div key={app.applicationId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
{app.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{app.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
</DropdownMenuGroup>
)}
{project.compose.length > 0 && (
<DropdownMenuGroup>
<DropdownMenuLabel>
Compose
</DropdownMenuLabel>
{project.compose.map((comp) => (
<div key={comp.composeId}>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="font-normal capitalize text-xs">
{comp.name}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{comp.domains.map((domain) => (
<DropdownMenuItem
key={domain.domainId}
asChild
>
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
>
<span>{domain.host}</span>
<ExternalLinkIcon className="size-4 shrink-0" />
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</div>
))}
</DropdownMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
) : null}
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2">
<span className="flex flex-col gap-1.5">
@@ -179,7 +261,10 @@ export const ShowProjects = () => {
<MoreHorizontalIcon className="size-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[200px] space-y-2">
<DropdownMenuContent
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuLabel className="font-normal">
Actions
</DropdownMenuLabel>

View File

@@ -61,6 +61,7 @@ export const AddCertificate = () => {
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync, isError, error, isLoading } =
api.certificates.create.useMutation();
const { data: servers } = api.server.withSSHKey.useQuery();
@@ -181,7 +182,7 @@ export const AddCertificate = () => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server (Optional)
Select a Server {!isCloud && "(Optional)"}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -202,7 +203,12 @@ export const AddCertificate = () => {
key={server.serverId}
value={server.serverId}
>
{server.name}
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>Servers ({servers?.length})</SelectLabel>

View File

@@ -50,7 +50,7 @@ export const ShowCertificates = () => {
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<ShieldCheck className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground">
<span className="text-base text-muted-foreground text-center">
You don't have any certificates created
</span>
<AddCertificate />

View File

@@ -28,7 +28,13 @@ 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, PenBoxIcon, PlusIcon } from "lucide-react";
import {
AlertTriangle,
Mail,
MessageCircleMore,
PenBoxIcon,
PlusIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -84,6 +90,15 @@ export const notificationSchema = z.discriminatedUnion("type", [
.min(1, { message: "At least one email is required" }),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("gotify"),
serverUrl: z.string().min(1, { message: "Server URL is required" }),
appToken: z.string().min(1, { message: "App Token is required" }),
priority: z.number().min(1).max(10).default(5),
decoration: z.boolean().default(true),
})
.merge(notificationBaseSchema),
]);
export const notificationsMap = {
@@ -103,6 +118,10 @@ export const notificationsMap = {
icon: <Mail size={29} className="text-muted-foreground" />,
label: "Email",
},
gotify: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
label: "Gotify",
},
};
export type NotificationSchema = z.infer<typeof notificationSchema>;
@@ -126,13 +145,14 @@ export const HandleNotifications = ({ notificationId }: Props) => {
);
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 { mutateAsync: testGotifyConnection, isLoading: isLoadingGotify } =
api.notification.testGotifyConnection.useMutation();
const slackMutation = notificationId
? api.notification.updateSlack.useMutation()
: api.notification.createSlack.useMutation();
@@ -145,6 +165,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const emailMutation = notificationId
? api.notification.updateEmail.useMutation()
: api.notification.createEmail.useMutation();
const gotifyMutation = notificationId
? api.notification.updateGotify.useMutation()
: api.notification.createGotify.useMutation();
const form = useForm<NotificationSchema>({
defaultValues: {
@@ -222,6 +245,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
name: notification.name,
dockerCleanup: notification.dockerCleanup,
});
} else if (notification.notificationType === "gotify") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
type: notification.notificationType,
appToken: notification.gotify?.appToken,
decoration: notification.gotify?.decoration || undefined,
priority: notification.gotify?.priority,
serverUrl: notification.gotify?.serverUrl,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
});
}
} else {
form.reset();
@@ -233,6 +270,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
telegram: telegramMutation,
discord: discordMutation,
email: emailMutation,
gotify: gotifyMutation,
};
const onSubmit = async (data: NotificationSchema) => {
@@ -300,6 +338,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
emailId: notification?.emailId || "",
});
} else if (data.type === "gotify") {
promise = gotifyMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
serverUrl: data.serverUrl,
appToken: data.appToken,
priority: data.priority,
name: data.name,
dockerCleanup: dockerCleanup,
decoration: data.decoration,
notificationId: notificationId || "",
gotifyId: notification?.gotifyId || "",
});
}
if (promise) {
@@ -700,6 +753,94 @@ export const HandleNotifications = ({ notificationId }: Props) => {
</Button>
</>
)}
{type === "gotify" && (
<>
<FormField
control={form.control}
name="serverUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL</FormLabel>
<FormControl>
<Input
placeholder="https://gotify.example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appToken"
render={({ field }) => (
<FormItem>
<FormLabel>App Token</FormLabel>
<FormControl>
<Input
placeholder="AzxcvbnmKjhgfdsa..."
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
defaultValue={5}
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Priority</FormLabel>
<FormControl>
<Input
placeholder="5"
{...field}
onChange={(e) => {
const value = e.target.value;
if (value) {
const port = Number.parseInt(value);
if (port > 0 && port < 10) {
field.onChange(port);
}
}
}}
type="number"
/>
</FormControl>
<FormDescription>
Message priority (1-10, default: 5)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="decoration"
defaultValue={true}
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Decoration</FormLabel>
<FormDescription>
Decorate the notification with emojis.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
</div>
</div>
<div className="flex flex-col gap-4">
@@ -824,7 +965,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingSlack ||
isLoadingTelegram ||
isLoadingDiscord ||
isLoadingEmail
isLoadingEmail ||
isLoadingGotify
}
variant="secondary"
onClick={async () => {
@@ -853,6 +995,13 @@ export const HandleNotifications = ({ notificationId }: Props) => {
toAddresses: form.getValues("toAddresses"),
fromAddress: form.getValues("fromAddress"),
});
} else if (type === "gotify") {
await testGotifyConnection({
serverUrl: form.getValues("serverUrl"),
appToken: form.getValues("appToken"),
priority: form.getValues("priority"),
decoration: form.getValues("decoration"),
});
}
toast.success("Connection Success");
} catch (err) {

View File

@@ -13,7 +13,7 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Bell, Loader2, Mail, Trash2 } from "lucide-react";
import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { HandleNotifications } from "./handle-notifications";
@@ -47,7 +47,7 @@ export const ShowNotifications = () => {
{data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Bell />
<span className="text-base text-muted-foreground">
<span className="text-base text-muted-foreground text-center">
To send notifications it is required to set at least 1
provider.
</span>
@@ -83,6 +83,11 @@ export const ShowNotifications = () => {
<Mail className="size-6 text-muted-foreground" />
</div>
)}
{notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" />
</div>
)}
{notification.name}
</span>

View File

@@ -22,7 +22,7 @@ import { Textarea } from "@/components/ui/textarea";
import { sshKeyCreate, type sshKeyType } from "@/server/db/validations";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { DownloadIcon, PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -111,6 +111,26 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
toast.error("Error generating the SSH Key");
});
const downloadKey = (
content: string,
defaultFilename: string,
keyType: "private" | "public",
) => {
const keyName = form.watch("name");
const filename = keyName
? `${keyName}${sshKeyId ? `_${sshKeyId}` : ""}_${keyType}_${defaultFilename}`
: `${keyType}_${defaultFilename}`;
const blob = new Blob([content], { type: "text/plain" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
@@ -245,7 +265,41 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
</FormItem>
)}
/>
<DialogFooter>
<DialogFooter className="flex items-center justify-between">
<div className="flex items-center gap-4">
{form.watch("privateKey") && (
<Button
type="button"
variant="outline"
size="default"
onClick={() =>
downloadKey(form.watch("privateKey"), "id_rsa", "private")
}
className="flex items-center gap-2"
>
<DownloadIcon className="h-4 w-4" />
Private Key
</Button>
)}
{form.watch("publicKey") && (
<Button
type="button"
variant="outline"
size="default"
onClick={() =>
downloadKey(
form.watch("publicKey"),
"id_rsa.pub",
"public",
)
}
className="flex items-center gap-2"
>
<DownloadIcon className="h-4 w-4" />
Public Key
</Button>
)}
</div>
<Button isLoading={isLoading} type="submit">
{sshKeyId ? "Update" : "Create"}
</Button>

View File

@@ -30,8 +30,8 @@ import { toast } from "sonner";
import { z } from "zod";
const addPermissions = z.object({
accesedProjects: z.array(z.string()).optional(),
accesedServices: z.array(z.string()).optional(),
accessedProjects: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional().default(false),
canCreateServices: z.boolean().optional().default(false),
canDeleteProjects: z.boolean().optional().default(false),
@@ -66,8 +66,8 @@ export const AddUserPermissions = ({ userId }: Props) => {
const form = useForm<AddPermissions>({
defaultValues: {
accesedProjects: [],
accesedServices: [],
accessedProjects: [],
accessedServices: [],
},
resolver: zodResolver(addPermissions),
});
@@ -75,8 +75,8 @@ export const AddUserPermissions = ({ userId }: Props) => {
useEffect(() => {
if (data) {
form.reset({
accesedProjects: data.accesedProjects || [],
accesedServices: data.accesedServices || [],
accessedProjects: data.accessedProjects || [],
accessedServices: data.accessedServices || [],
canCreateProjects: data.canCreateProjects,
canCreateServices: data.canCreateServices,
canDeleteProjects: data.canDeleteProjects,
@@ -98,8 +98,8 @@ export const AddUserPermissions = ({ userId }: Props) => {
canDeleteServices: data.canDeleteServices,
canDeleteProjects: data.canDeleteProjects,
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
accesedProjects: data.accesedProjects || [],
accesedServices: data.accesedServices || [],
accessedProjects: data.accessedProjects || [],
accessedServices: data.accessedServices || [],
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
@@ -318,7 +318,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
/>
<FormField
control={form.control}
name="accesedProjects"
name="accessedProjects"
render={() => (
<FormItem className="md:col-span-2">
<div className="mb-4">
@@ -339,7 +339,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
<FormField
key={`project-${index}`}
control={form.control}
name="accesedProjects"
name="accessedProjects"
render={({ field }) => {
return (
<FormItem
@@ -380,7 +380,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
<FormField
key={`project-${index}`}
control={form.control}
name="accesedServices"
name="accessedServices"
render={({ field }) => {
return (
<FormItem

View File

@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { FitAddon } from "xterm-addon-fit";
import "@xterm/xterm/css/xterm.css";
import { AttachAddon } from "@xterm/addon-attach";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { useTheme } from "next-themes";
import { getLocalServerData } from "./local-server-config";
@@ -37,6 +38,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
foreground: "currentColor",
},
});
const addonFit = new FitAddon();
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
@@ -54,6 +56,8 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
const ws = new WebSocket(wsUrl);
const addonAttach = new AttachAddon(ws);
const clipboardAddon = new ClipboardAddon();
term.loadAddon(clipboardAddon);
// @ts-ignore
term.open(termRef.current);
@@ -68,7 +72,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
return (
<div className="flex flex-col gap-4">
<div className="w-full h-full bg-transparent border rounded-lg p-2 ">
<div className="w-full h-full bg-transparent border rounded-lg p-2">
<div id={id} ref={termRef} className="rounded-xl" />
</div>
</div>

View File

@@ -60,7 +60,7 @@ export function NodeCard({ node, serverId }: Props) {
<div className="font-medium">{node.Hostname}</div>
<Badge variant="green">{node.ManagerStatus || "Worker"}</Badge>
</div>
<div className="flex flex-wrap items-center space-x-4">
<div className="flex flex-wrap items-center gap-4">
<Badge variant="green">TLS Status: {node.TLSStatus}</Badge>
<Badge variant="blue">Availability: {node.Availability}</Badge>
</div>

View File

@@ -72,7 +72,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
return (
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
<header className="flex items-center justify-between">
<header className="flex items-center flex-wrap gap-4 justify-between">
<div className="space-y-1">
<CardTitle className="text-xl flex flex-row gap-2">
<WorkflowIcon className="size-6 text-muted-foreground self-center" />

View File

@@ -5,9 +5,5 @@ interface Props {
}
export const DashboardLayout = ({ children }: Props) => {
return (
<Page>
<div>{children}</div>
</Page>
);
return <Page>{children}</Page>;
};

View File

@@ -11,7 +11,7 @@ interface Props {
export const OnboardingLayout = ({ children }: Props) => {
return (
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
<div className="relative hidden h-full flex-col p-10 text-white dark:border-r lg:flex">
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
<div className="absolute inset-0 bg-muted" />
<Link
href="https://dokploy.com"
@@ -22,35 +22,16 @@ export const OnboardingLayout = ({ children }: Props) => {
</Link>
<div className="relative z-20 mt-auto">
<blockquote className="space-y-2">
<p className="text-lg">
<p className="text-lg text-primary">
&ldquo;The Open Source alternative to Netlify, Vercel,
Heroku.&rdquo;
</p>
{/* <footer className="text-sm">Sofia Davis</footer> */}
</blockquote>
</div>
</div>
<div className="w-full">
<div className="flex w-full flex-col justify-center space-y-6 max-w-lg mx-auto">
{children}
{/* <p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</Link>
.
</p> */}
</div>
<div className="flex items-center gap-4 justify-center absolute bottom-4 right-4 text-muted-foreground">
<Button variant="ghost" size="icon">

View File

@@ -5,9 +5,5 @@ interface Props {
}
export const ProjectLayout = ({ children }: Props) => {
return (
<div>
<Page>{children}</Page>
</div>
);
return <Page>{children}</Page>;
};

File diff suppressed because it is too large Load Diff

View File

@@ -118,14 +118,16 @@ export const UserNav = () => {
</DropdownMenuItem>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings/server");
}}
>
Settings
</DropdownMenuItem>
{data?.rol === "admin" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</>
) : (
<>
@@ -147,6 +149,17 @@ export const UserNav = () => {
Servers
</DropdownMenuItem>
)}
{data?.rol === "admin" && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/settings");
}}
>
Settings
</DropdownMenuItem>
)}
</>
)}
</DropdownMenuGroup>

View File

@@ -28,7 +28,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
<BreadcrumbList>
{list.map((item, index) => (
<Fragment key={item.name}>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbItem className="block">
<BreadcrumbLink href={item.href} asChild={!!item.href}>
{item.href ? (
<Link href={item.href}>{item.name}</Link>
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
)}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbSeparator className="block" />
</Fragment>
))}
</BreadcrumbList>

View File

@@ -17,7 +17,7 @@ import {
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
export const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
@@ -329,7 +329,7 @@ const SidebarInset = React.forwardRef<
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}

View File

@@ -0,0 +1,15 @@
ALTER TYPE "notificationType" ADD VALUE 'gotify';--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "gotify" (
"gotifyId" text PRIMARY KEY NOT NULL,
"serverUrl" text NOT NULL,
"appToken" text NOT NULL,
"priority" integer DEFAULT 5 NOT NULL,
"decoration" boolean
);
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "gotifyId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_gotifyId_gotify_gotifyId_fk" FOREIGN KEY ("gotifyId") REFERENCES "public"."gotify"("gotifyId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "admin" ADD COLUMN "cleanupCacheApplications" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "cleanupCacheOnCompose" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "deployment" ADD COLUMN "errorMessage" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" ALTER COLUMN "cleanupCacheApplications" SET DEFAULT false;

View File

@@ -0,0 +1,3 @@
-- Custom SQL migration file, put you code below!
UPDATE "admin" SET "cleanupCacheApplications" = false;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -393,6 +393,41 @@
"when": 1736669623831,
"tag": "0055_next_serpent_society",
"breakpoints": true
},
{
"idx": 56,
"version": "6",
"when": 1736789918294,
"tag": "0056_majestic_skaar",
"breakpoints": true
},
{
"idx": 57,
"version": "6",
"when": 1737306063563,
"tag": "0057_tricky_living_tribunal",
"breakpoints": true
},
{
"idx": 58,
"version": "6",
"when": 1737612903012,
"tag": "0058_brown_sharon_carter",
"breakpoints": true
},
{
"idx": 59,
"version": "6",
"when": 1737615160768,
"tag": "0059_striped_bill_hollister",
"breakpoints": true
},
{
"idx": 60,
"version": "6",
"when": 1737929896838,
"tag": "0060_disable-aggressive-cache",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,7 @@
export const Languages = {
english: { code: "en", name: "English" },
polish: { code: "pl", name: "Polski" },
ukrainian: { code: "uk", name: "Українська" },
russian: { code: "ru", name: "Русский" },
french: { code: "fr", name: "Français" },
german: { code: "de", name: "Deutsch" },
@@ -17,6 +18,7 @@ export const Languages = {
norwegian: { code: "no", name: "Norsk" },
azerbaijani: { code: "az", name: "Azərbaycan" },
indonesian: { code: "id", name: "Bahasa Indonesia" },
malayalam: { code: "ml", name: "മലയാളം" },
};
export type Language = keyof typeof Languages;

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.17.2",
"version": "v0.17.9",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -35,6 +35,18 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"bl": "6.0.11",
"rotating-file-stream": "3.2.3",
"qrcode": "^1.5.3",
"otpauth": "^9.2.3",
"hi-base32": "^0.5.1",
"boxen": "^7.1.1",
"@octokit/auth-app": "^6.0.4",
"nodemailer": "6.9.14",
"@react-email/components": "^0.0.21",
"node-os-utils": "1.3.7",
"@lucia-auth/adapter-drizzle": "1.0.7",
"dockerode": "4.0.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
@@ -75,6 +87,7 @@
"@uiw/react-codemirror": "^4.22.1",
"@xterm/addon-attach": "0.10.0",
"@xterm/xterm": "^5.4.0",
"@xterm/addon-clipboard": "0.1.0",
"adm-zip": "^0.5.14",
"bcrypt": "5.1.1",
"bullmq": "5.4.2",
@@ -127,6 +140,9 @@
"@faker-js/faker": "^8.4.1"
},
"devDependencies": {
"@types/qrcode": "^1.5.5",
"@types/nodemailer": "^6.4.15",
"@types/node-os-utils": "1.3.4",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/js-cookie": "^3.0.6",

View File

@@ -71,6 +71,25 @@ export default async function handler(
return;
}
// skip workflow runs use keywords
// @link https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/skipping-workflow-runs
if (
[
"[skip ci]",
"[ci skip]",
"[no ci]",
"[skip actions]",
"[actions skip]",
].find((keyword) =>
extractCommitMessage(req.headers, req.body).includes(keyword),
)
) {
res.status(200).json({
message: "Deployment skipped: commit message contains skip keyword",
});
return;
}
if (req.headers["x-github-event"] === "push") {
try {
const branchName = githubBody?.ref?.replace("refs/heads/", "");
@@ -102,7 +121,7 @@ export default async function handler(
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(
"deployments",
@@ -137,7 +156,7 @@ export default async function handler(
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(
@@ -163,8 +182,9 @@ export default async function handler(
}
} else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id;
const action = githubBody?.action;
if (githubBody?.action === "closed") {
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
@@ -182,79 +202,86 @@ export default async function handler(
res.status(200).json({ message: "Preview Deployment Closed" });
return;
}
// opened or synchronize or reopened
const repository = githubBody?.repository?.name;
const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login;
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const repository = githubBody?.repository?.name;
const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, repository),
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
),
with: {
previewDeployments: true,
},
});
const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
previewDeploymentId = previewDeployment.previewDeploymentId;
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, repository),
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
),
with: {
previewDeployments: true,
},
);
});
const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
previewDeploymentId = previewDeployment.previewDeploymentId;
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
return res.status(200).json({ message: "Apps Deployed" });
}
return res.status(200).json({ message: "Apps Deployed" });
}
return res.status(400).json({ message: "No Actions matched" });

View File

@@ -239,8 +239,8 @@ const Project = (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center">
<CardHeader className="">
<div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className="p-0">
<CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" />
{data?.name}
@@ -248,7 +248,7 @@ const Project = (
<CardDescription>{data?.description}</CardDescription>
</CardHeader>
{(auth?.rol === "admin" || user?.canCreateServices) && (
<div className="flex flex-row gap-4 flex-wrap px-4">
<div className="flex flex-row gap-4 flex-wrap">
<ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button>
</ProjectEnvironment>

View File

@@ -13,6 +13,7 @@ import { ShowGeneralApplication } from "@/components/dashboard/application/gener
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
@@ -82,8 +83,6 @@ const Service = (
},
);
const { mutateAsync, isLoading: isRemoving } =
api.application.delete.useMutation();
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
@@ -177,34 +176,7 @@ const Service = (
<div className="flex flex-row gap-2 justify-end">
<UpdateApplication applicationId={applicationId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Delete Application"
description="Are you sure you want to delete this application?"
type="destructive"
onClick={async () => {
await mutateAsync({
applicationId: applicationId,
})
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Application deleted successfully");
})
.catch(() => {
toast.error("Error deleting application");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={applicationId} type="application" />
)}
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
@@ -168,7 +168,7 @@ const Service = (
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
<DeleteService id={composeId} type="compose" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMariadbCredentials } from "@/components/dashboard/mariadb/general/show-external-mariadb-credentials";
import { ShowGeneralMariadb } from "@/components/dashboard/mariadb/general/show-general-mariadb";
@@ -67,8 +68,7 @@ const Mariadb = (
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mariadb.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -148,35 +148,10 @@ const Mariadb = (
</TooltipProvider>
)}
</div>
<div className="flex flex-row gap-2">
<div className="flex flex-row gap-2 justify-end">
<UpdateMariadb mariadbId={mariadbId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Mariadb"
description="Are you sure you want to delete this mariadb?"
type="destructive"
onClick={async () => {
await remove({ mariadbId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mariadb deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mariadb");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={mariadbId} type="mariadb" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ShowExternalMongoCredentials } from "@/components/dashboard/mongo/general/show-external-mongo-credentials";
import { ShowGeneralMongo } from "@/components/dashboard/mongo/general/show-general-mongo";
@@ -69,8 +70,6 @@ const Mongo = (
enabled: !!auth?.id && auth?.rol === "user",
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mongo.remove.useMutation();
return (
<div className="pb-10">
@@ -155,32 +154,7 @@ const Mongo = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMongo mongoId={mongoId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove mongo"
description="Are you sure you want to delete this mongo?"
type="destructive"
onClick={async () => {
await remove({ mongoId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mongo deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mongo");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={mongoId} type="mongo" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ShowExternalMysqlCredentials } from "@/components/dashboard/mysql/general/show-external-mysql-credentials";
@@ -68,8 +69,6 @@ const MySql = (
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.mysql.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -154,32 +153,7 @@ const MySql = (
<div className="flex flex-row gap-2 justify-end">
<UpdateMysql mysqlId={mysqlId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Mysql"
description="Are you sure you want to delete this mysql?"
type="destructive"
onClick={async () => {
await remove({ mysqlId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Mysql deleted successfully");
})
.catch(() => {
toast.error("Error deleting the mysql");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={mysqlId} type="mysql" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
@@ -70,9 +71,6 @@ const Postgresql = (
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.postgres.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -156,32 +154,7 @@ const Postgresql = (
<div className="flex flex-row gap-2 justify-end">
<UpdatePostgres postgresId={postgresId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Postgres"
description="Are you sure you want to delete this postgres?"
type="destructive"
onClick={async () => {
await remove({ postgresId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Postgres deleted successfully");
})
.catch(() => {
toast.error("Error deleting the postgres");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={postgresId} type="postgres" />
)}
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { ShowResources } from "@/components/dashboard/application/advanced/show-
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowExternalRedisCredentials } from "@/components/dashboard/redis/general/show-external-redis-credentials";
@@ -68,8 +69,6 @@ const Redis = (
},
);
const { mutateAsync: remove, isLoading: isRemoving } =
api.redis.remove.useMutation();
return (
<div className="pb-10">
<BreadcrumbSidebar
@@ -153,32 +152,7 @@ const Redis = (
<div className="flex flex-row gap-2 justify-end">
<UpdateRedis redisId={redisId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DialogAction
title="Remove Redis"
description="Are you sure you want to delete this redis?"
type="destructive"
onClick={async () => {
await remove({ redisId })
.then(() => {
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Redis deleted successfully");
})
.catch(() => {
toast.error("Error deleting the redis");
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10 "
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
<DeleteService id={redisId} type="redis" />
)}
</div>
</div>

View File

@@ -0,0 +1,220 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { DialogFooter } from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { Settings } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import React, { useEffect, type ReactElement } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import superjson from "superjson";
import { z } from "zod";
const settings = z.object({
cleanCacheOnApplications: z.boolean(),
cleanCacheOnCompose: z.boolean(),
cleanCacheOnPreviews: z.boolean(),
});
type SettingsType = z.infer<typeof settings>;
const Page = () => {
const { data, refetch } = api.admin.one.useQuery();
const { mutateAsync, isLoading, isError, error } =
api.admin.update.useMutation();
const form = useForm<SettingsType>({
defaultValues: {
cleanCacheOnApplications: false,
cleanCacheOnCompose: false,
cleanCacheOnPreviews: false,
},
resolver: zodResolver(settings),
});
useEffect(() => {
form.reset({
cleanCacheOnApplications: data?.cleanupCacheApplications || false,
cleanCacheOnCompose: data?.cleanupCacheOnCompose || false,
cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false,
});
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (values: SettingsType) => {
await mutateAsync({
cleanupCacheApplications: values.cleanCacheOnApplications,
cleanupCacheOnCompose: values.cleanCacheOnCompose,
cleanupCacheOnPreviews: values.cleanCacheOnPreviews,
})
.then(() => {
toast.success("Settings updated");
refetch();
})
.catch(() => {
toast.error("Something went wrong");
});
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Settings className="size-6 text-muted-foreground self-center" />
Settings
</CardTitle>
<CardDescription>Manage your Dokploy settings</CardDescription>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
<Form {...form}>
<form
id="hook-form-add-security"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-2"
>
<FormField
control={form.control}
name="cleanCacheOnApplications"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Applications</FormLabel>
<FormDescription>
Clean the cache after every application deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnPreviews"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Previews</FormLabel>
<FormDescription>
Clean the cache after every preview deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnCompose"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Compose</FormLabel>
<FormDescription>
Clean the cache after every compose deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-add-security"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</CardContent>
</div>
</Card>
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Server">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
if (user.rol === "user") {
return {
redirect: {
permanent: true,
destination: "/dashboard/settings/profile",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -8,11 +8,7 @@ import type { ReactElement } from "react";
import superjson from "superjson";
const Dashboard = () => {
return (
<>
<SwarmMonitorCard />
</>
);
return <SwarmMonitorCard />;
};
export default Dashboard;

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,58 @@
{
"settings.common.save": "സേവ് ചെയ്യുക",
"settings.common.enterTerminal": "ടർമിനലിൽ പ്രവേശിക്കുക",
"settings.server.domain.title": "സർവർ ഡോമെയ്ൻ",
"settings.server.domain.description": "നിങ്ങളുടെ സർവർ അപ്ലിക്കേഷനിൽ ഒരു ഡോമെയ്ൻ ചേർക്കുക.",
"settings.server.domain.form.domain": "ഡോമെയ്ൻ",
"settings.server.domain.form.letsEncryptEmail": "ലെറ്റ്സ് എൻക്രിപ്റ്റ് ഇമെയിൽ",
"settings.server.domain.form.certificate.label": "സർട്ടിഫിക്കറ്റ് പ്രൊവൈഡർ",
"settings.server.domain.form.certificate.placeholder": "ഒരു സർട്ടിഫിക്കറ്റ് തിരഞ്ഞെടുക്കുക",
"settings.server.domain.form.certificateOptions.none": "ഒന്നുമില്ല",
"settings.server.domain.form.certificateOptions.letsencrypt": "ലെറ്റ്സ് എൻക്രിപ്റ്റ്",
"settings.server.webServer.title": "വെബ് സർവർ",
"settings.server.webServer.description": "വെബ് സർവർ റീലോഡ് ചെയ്യുക അല്ലെങ്കിൽ ശുചീകരിക്കുക.",
"settings.server.webServer.actions": "നടപടികൾ",
"settings.server.webServer.reload": "റീലോഡ് ചെയ്യുക",
"settings.server.webServer.watchLogs": "ലോഗുകൾ കാണുക",
"settings.server.webServer.updateServerIp": "സർവർ IP അപ്ഡേറ്റ് ചെയ്യുക",
"settings.server.webServer.server.label": "സർവർ",
"settings.server.webServer.traefik.label": "ട്രാഫിക്",
"settings.server.webServer.traefik.modifyEnv": "ചുറ്റുപാടുകൾ മാറ്റുക",
"settings.server.webServer.traefik.managePorts": "അധിക പോർട്ട് മാപ്പിംഗ്",
"settings.server.webServer.traefik.managePortsDescription": "ട്രാഫിക്കിനായി അധിക പോർട്ടുകൾ ചേർക്കുക അല്ലെങ്കിൽ നീക്കം ചെയ്യുക",
"settings.server.webServer.traefik.targetPort": "ടാർഗറ്റ് പോർട്ട്",
"settings.server.webServer.traefik.publishedPort": "പ്രസിദ്ധീകരിച്ച പോർട്ട്",
"settings.server.webServer.traefik.addPort": "പോർട്ട് ചേർക്കുക",
"settings.server.webServer.traefik.portsUpdated": "പോർട്ടുകൾ വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു",
"settings.server.webServer.traefik.portsUpdateError": "പോർട്ടുകൾ അപ്ഡേറ്റ് ചെയ്യാൻ പരാജയപ്പെട്ടു",
"settings.server.webServer.traefik.publishMode": "പ്രസിദ്ധീകരണ മോഡ്",
"settings.server.webServer.storage.label": "ഇടം",
"settings.server.webServer.storage.cleanUnusedImages": "ഉപയോഗിക്കാത്ത ഇമേജുകൾ ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanUnusedVolumes": "ഉപയോഗിക്കാത്ത വോള്യങ്ങൾ ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanStoppedContainers": "നിർത്തിയ കണ്ടെയ്‌നറുകൾ ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanDockerBuilder": "ഡോക്കർ ബിൽഡറും സിസ്റ്റവും ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanMonitoring": "മോണിറ്ററിംഗ് ശുചീകരിക്കുക",
"settings.server.webServer.storage.cleanAll": "എല്ലാം ശുചീകരിക്കുക",
"settings.profile.title": "അക്കൗണ്ട്",
"settings.profile.description": "നിങ്ങളുടെ പ്രൊഫൈൽ വിശദാംശങ്ങൾ ഇവിടെ മാറ്റുക.",
"settings.profile.email": "ഇമെയിൽ",
"settings.profile.password": "പാസ്വേഡ്",
"settings.profile.avatar": "അവതാർ",
"settings.appearance.title": "ദൃശ്യമാനം",
"settings.appearance.description": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന്റെ തീം ഇഷ്ടാനുസൃതമാക്കുക.",
"settings.appearance.theme": "തീം",
"settings.appearance.themeDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു തീം തിരഞ്ഞെടുക്കുക",
"settings.appearance.themes.light": "ലൈറ്റ്",
"settings.appearance.themes.dark": "ഡാർക്ക്",
"settings.appearance.themes.system": "സിസ്റ്റം",
"settings.appearance.language": "ഭാഷ",
"settings.appearance.languageDescription": "നിങ്ങളുടെ ഡാഷ്ബോർഡിന് ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക",
"settings.terminal.connectionSettings": "കണക്ഷൻ ക്രമീകരണങ്ങൾ",
"settings.terminal.ipAddress": "IP വിലാസം",
"settings.terminal.port": "പോർട്ട്",
"settings.terminal.username": "ഉപയോക്തൃനാമം"
}

View File

@@ -1,5 +1,6 @@
{
"settings.common.save": "Сохранить",
"settings.common.enterTerminal": "Открыть терминал",
"settings.server.domain.title": "Домен сервера",
"settings.server.domain.description": "Установите домен для вашего серверного приложения Dokploy.",
"settings.server.domain.form.domain": "Домен",
@@ -7,18 +8,26 @@
"settings.server.domain.form.certificate.label": "Сертификат",
"settings.server.domain.form.certificate.placeholder": "Выберите сертификат",
"settings.server.domain.form.certificateOptions.none": "Нет",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (По умолчанию)",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Веб-сервер",
"settings.server.webServer.description": "Перезагрузка или очистка веб-сервера.",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.storage.label": "Дисковое пространство",
"settings.server.webServer.actions": "Действия",
"settings.server.webServer.reload": "Перезагрузить",
"settings.server.webServer.watchLogs": "Просмотр логов",
"settings.server.webServer.updateServerIp": "Изменить IP адрес",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Изменить переменные окружения",
"settings.server.webServer.traefik.managePorts": "Назначение портов",
"settings.server.webServer.traefik.managePortsDescription": "Добавить или удалить дополнительные порты для Traefik",
"settings.server.webServer.traefik.targetPort": "Внутренний порт",
"settings.server.webServer.traefik.publishedPort": "Внешний порт",
"settings.server.webServer.traefik.addPort": "Добавить порт",
"settings.server.webServer.traefik.portsUpdated": "Порты успешно обновлены",
"settings.server.webServer.traefik.portsUpdateError": "Не удалось обновить порты",
"settings.server.webServer.traefik.publishMode": "Режим сопоставления",
"settings.server.webServer.storage.label": "Дисковое пространство",
"settings.server.webServer.storage.cleanUnusedImages": "Очистить неиспользуемые образы",
"settings.server.webServer.storage.cleanUnusedVolumes": "Очистить неиспользуемые тома",
"settings.server.webServer.storage.cleanStoppedContainers": "Очистить остановленные контейнеры",
@@ -40,5 +49,10 @@
"settings.appearance.themes.dark": "Темная",
"settings.appearance.themes.system": "Системная",
"settings.appearance.language": "Язык",
"settings.appearance.languageDescription": "Select a language for your dashboard"
"settings.appearance.languageDescription": "Выберите язык для панели управления",
"settings.terminal.connectionSettings": "Настройки подключения",
"settings.terminal.ipAddress": "IP адрес",
"settings.terminal.port": "Порт",
"settings.terminal.username": "Имя пользователя"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,58 @@
{
"settings.common.save": "Зберегти",
"settings.common.enterTerminal": "Увійти в термінал",
"settings.server.domain.title": "Домен сервера",
"settings.server.domain.description": "Додайте домен до вашого серверного застосунку.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Електронна пошта для Let's Encrypt",
"settings.server.domain.form.certificate.label": "Постачальник сертифікатів",
"settings.server.domain.form.certificate.placeholder": "Оберіть сертифікат",
"settings.server.domain.form.certificateOptions.none": "Відсутній",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
"settings.server.webServer.title": "Веб-сервер",
"settings.server.webServer.description": "Перезавантажте або очистьте веб-сервер.",
"settings.server.webServer.actions": "Дії",
"settings.server.webServer.reload": "Перезавантажити",
"settings.server.webServer.watchLogs": "Перегляд логів",
"settings.server.webServer.updateServerIp": "Оновити IP-адресу сервера",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Змінити середовище",
"settings.server.webServer.traefik.managePorts": "Додаткові порти",
"settings.server.webServer.traefik.managePortsDescription": "Додайте або видаліть порти для Traefik",
"settings.server.webServer.traefik.targetPort": "Цільовий порт",
"settings.server.webServer.traefik.publishedPort": "Опублікований порт",
"settings.server.webServer.traefik.addPort": "Додати порт",
"settings.server.webServer.traefik.portsUpdated": "Порти успішно оновлено",
"settings.server.webServer.traefik.portsUpdateError": "Не вдалося оновити порти",
"settings.server.webServer.traefik.publishMode": "Режим публікації",
"settings.server.webServer.storage.label": "Дисковий простір",
"settings.server.webServer.storage.cleanUnusedImages": "Очистити невикористані образи",
"settings.server.webServer.storage.cleanUnusedVolumes": "Очистити невикористані томи",
"settings.server.webServer.storage.cleanStoppedContainers": "Очистити зупинені контейнери",
"settings.server.webServer.storage.cleanDockerBuilder": "Очистити Docker Builder і систему",
"settings.server.webServer.storage.cleanMonitoring": "Очистити моніторинг",
"settings.server.webServer.storage.cleanAll": "Очистити все",
"settings.profile.title": "Обліковий запис",
"settings.profile.description": "Змініть дані вашого профілю.",
"settings.profile.email": "Електронна пошта",
"settings.profile.password": "Пароль",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Зовнішній вигляд",
"settings.appearance.description": "Налаштуйте тему вашої панелі керування.",
"settings.appearance.theme": "Тема",
"settings.appearance.themeDescription": "Оберіть тему для вашої панелі керування",
"settings.appearance.themes.light": "Світла",
"settings.appearance.themes.dark": "Темна",
"settings.appearance.themes.system": "Системна",
"settings.appearance.language": "Мова",
"settings.appearance.languageDescription": "Оберіть мову для вашої панелі керування",
"settings.terminal.connectionSettings": "Налаштування з'єднання",
"settings.terminal.ipAddress": "IP-адреса",
"settings.terminal.port": "Порт",
"settings.terminal.username": "Ім'я користувача"
}

View File

@@ -0,0 +1,10 @@
<svg width="1252" height="1252" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<g id="#70c6beff">
<path id="svg_2" d="m634.37,138.38c11.88,-1.36 24.25,1.3 34.18,8.09c14.96,9.66 25.55,24.41 34.49,39.51c40.59,68.03 81.45,135.91 122.02,203.96c54.02,90.99 108.06,181.97 161.94,273.06c37.28,63 74.65,125.96 112.18,188.82c24.72,41.99 50.21,83.54 73.84,126.16c10.18,17.84 15.77,38.44 14.93,59.03c-0.59,15.92 -3.48,32.28 -11.84,46.08c-11.73,19.46 -31.39,33.2 -52.71,40.36c-11.37,4.09 -23.3,6.87 -35.43,6.89c-132.32,-0.05 -264.64,0.04 -396.95,0.03c-11.38,-0.29 -22.95,-1.6 -33.63,-5.72c-7.81,-3.33 -15.5,-7.43 -21.61,-13.42c-10.43,-10.32 -17.19,-24.96 -15.38,-39.83c0.94,-10.39 3.48,-20.64 7.76,-30.16c4.15,-9.77 9.99,-18.67 15.06,-27.97c22.13,-39.47 45.31,-78.35 69.42,-116.65c7.72,-12.05 14.44,-25.07 25.12,-34.87c11.35,-10.39 25.6,-18.54 41.21,-19.6c12.55,-0.52 24.89,3.82 35.35,10.55c11.8,6.92 21.09,18.44 24.2,31.88c4.49,17.01 -0.34,34.88 -7.55,50.42c-8.09,17.65 -19.62,33.67 -25.81,52.18c-1.13,4.21 -2.66,9.52 0.48,13.23c3.19,3 7.62,4.18 11.77,5.22c12,2.67 24.38,1.98 36.59,2.06c45,-0.01 90,0 135,0c8.91,-0.15 17.83,0.3 26.74,-0.22c6.43,-0.74 13.44,-1.79 18.44,-6.28c3.3,-2.92 3.71,-7.85 2.46,-11.85c-2.74,-8.86 -7.46,-16.93 -12.12,-24.89c-119.99,-204.91 -239.31,-410.22 -360.56,-614.4c-3.96,-6.56 -7.36,-13.68 -13.03,-18.98c-2.8,-2.69 -6.95,-4.22 -10.77,-3.11c-3.25,1.17 -5.45,4.03 -7.61,6.57c-5.34,6.81 -10.12,14.06 -14.51,21.52c-20.89,33.95 -40.88,68.44 -61.35,102.64c-117.9,198.43 -235.82,396.85 -353.71,595.29c-7.31,13.46 -15.09,26.67 -23.57,39.43c-7.45,10.96 -16.49,21.23 -28.14,27.83c-13.73,7.94 -30.69,11.09 -46.08,6.54c-11.23,-3.47 -22.09,-9.12 -30.13,-17.84c-10.18,-10.08 -14.69,-24.83 -14.17,-38.94c0.52,-14.86 5.49,-29.34 12.98,-42.1c71.58,-121.59 143.62,-242.92 215.93,-364.09c37.2,-62.8 74.23,-125.69 111.64,-188.36c37.84,-63.5 75.77,-126.94 113.44,-190.54c21.02,-35.82 42.19,-71.56 64.28,-106.74c6.79,-11.15 15.58,-21.15 26.16,-28.85c8.68,-5.92 18.42,-11 29.05,-11.94z" fill="#70c6be"/>
</g>
<g id="#1ba0d8ff">
<path id="svg_3" d="m628.35,608.38c17.83,-2.87 36.72,1.39 51.5,11.78c11.22,8.66 19.01,21.64 21.26,35.65c1.53,10.68 0.49,21.75 -3.44,31.84c-3.02,8.73 -7.35,16.94 -12.17,24.81c-68.76,115.58 -137.5,231.17 -206.27,346.75c-8.8,14.47 -16.82,29.47 -26.96,43.07c-7.37,9.11 -16.58,16.85 -27.21,21.89c-22.47,11.97 -51.79,4.67 -68.88,-13.33c-8.66,-8.69 -13.74,-20.63 -14.4,-32.84c-0.98,-12.64 1.81,-25.42 7.53,-36.69c5.03,-10.96 10.98,-21.45 17.19,-31.77c30.22,-50.84 60.17,-101.84 90.3,-152.73c41.24,-69.98 83.16,-139.55 124.66,-209.37c4.41,-7.94 9.91,-15.26 16.09,-21.9c8.33,-8.46 18.9,-15.3 30.8,-17.16z" fill="#1ba0d8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,5 @@
<svg width="101" height="100" viewBox="0 0 101 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M69.3721 38.0715H61.6579V30.3573C61.6579 25.4287 57.6579 21.4287 52.7293 21.4287H46.7649C41.8363 21.4287 37.8363 25.4287 37.8363 30.3573V38.0715H22.9792V45.2143H44.9792V28.5716H54.515V45.2143H71.1578V54.75H61.6221H54.515V71.393H44.9792V54.75H22.9792V61.8928H37.8363V69.6072C37.8363 74.5358 41.8363 78.5358 46.7649 78.5358H52.7293C57.6579 78.5358 61.6579 74.5358 61.6579 69.6072V61.8928H69.3721C74.3006 61.8928 78.3006 57.8928 78.3006 52.9643V47C78.3006 42.0715 74.3006 38.0715 69.3721 38.0715Z" fill="white"/>
<path d="M72.0506 0H29.1935C13.4139 0 0.62207 12.7919 0.62207 28.5714V71.4286C0.62207 87.2081 13.4139 100 29.1935 100H72.0506C87.8302 100 100.622 87.2081 100.622 71.4286V28.5714C100.622 12.7919 87.8302 0 72.0506 0Z" fill="#0089FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.0067 32.945H66.3293H44.5235H33.6205V23.999H68.0067V32.945ZM44.5233 53.9122V66.2129H68.2861V75.1589H33.6204V44.9662H44.5233H66.3291V53.9122H44.5233Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,5 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100%" viewBox="0 0 180 180" enable-background="new 0 0 180 180" xml:space="preserve">
<path fill="#FEFEFE" opacity="1.000000" stroke="none" d=" M111.000000,181.000000 C74.000000,181.000000 37.500004,181.000000 1.000005,181.000000 C1.000003,121.000008 1.000003,61.000011 1.000002,1.000013 C60.999989,1.000009 120.999977,1.000009 180.999969,1.000004 C180.999985,60.999985 180.999985,120.999969 181.000000,180.999969 C157.833328,181.000000 134.666672,181.000000 111.000000,181.000000 M92.383194,23.019001 C81.313591,22.978577 81.347420,22.985394 79.133850,33.967770 C78.955315,34.853519 78.030640,35.938652 77.187553,36.284145 C71.550491,38.594177 65.841438,40.728523 60.079868,42.949146 C51.854774,36.325741 50.300980,36.060764 45.462185,40.534008 C35.819622,49.448120 35.819626,49.448124 43.224102,60.492462 C41.382980,64.713478 39.115070,68.685867 37.935665,72.958702 C36.732948,77.316010 35.234783,79.833847 30.057184,79.454468 C25.770687,79.140373 23.503710,82.094612 23.123014,86.367462 C22.170498,97.058365 23.448399,102.167702 28.351013,102.077286 C35.643330,101.942780 37.481285,106.068214 38.933784,111.529289 C38.983166,111.714943 39.436890,111.793068 40.802368,112.453255 C43.044167,109.879097 45.249161,106.642052 48.130058,104.203537 C52.509956,100.496193 54.125355,96.469872 53.950626,90.497017 C53.426945,72.595833 67.390541,56.483757 84.586967,54.011436 C104.170532,51.195923 121.810631,62.289631 127.042664,80.711487 C133.866669,104.738655 115.739006,128.447021 90.762108,128.272644 C88.059288,128.253754 84.679626,127.980476 82.777954,129.392670 C77.882332,133.028183 73.640266,137.543808 68.821976,141.999115 C70.466492,143.029861 70.972687,143.572098 71.558319,143.678986 C76.734505,144.623611 80.020096,146.547058 79.674500,152.950134 C79.451477,157.082016 83.111946,159.313339 87.371513,158.879456 C92.749634,158.331635 101.208298,161.697632 101.971764,153.709152 C102.722359,145.855438 106.998207,144.583542 112.480064,142.876846 C114.208542,142.338699 115.968300,141.598587 117.423370,140.548538 C120.332664,138.449066 122.307137,138.727737 125.134735,141.260239 C127.112488,143.031555 131.190781,144.421356 133.444077,143.586884 C144.141052,139.625412 146.261215,130.462555 138.985092,122.166840 C138.788422,121.942619 138.754318,121.575813 138.827591,121.776024 C140.987778,116.496361 143.289413,111.681892 144.889893,106.644653 C145.799866,103.780693 146.954834,102.739929 149.986267,102.681595 C156.969391,102.547234 158.883026,100.417870 158.972443,93.654068 C159.145615,80.552666 159.118896,80.521065 146.636948,78.797089 C144.032028,72.592224 141.475266,66.502083 138.827988,60.196339 C138.910278,60.044697 139.043243,59.482845 139.398239,59.192280 C146.726578,53.194103 145.816498,48.658714 138.725174,42.491474 C138.097733,41.945801 137.535004,41.326450 136.923462,40.761639 C132.204132,36.402893 129.404175,36.516243 124.469460,40.822025 C123.299767,41.842648 121.124886,42.989830 119.991692,42.558125 C114.297371,40.388798 108.795174,37.715183 103.089928,35.157913 C102.101974,24.233391 101.295547,23.213860 92.383194,23.019001 M117.068085,101.337982 C117.646690,96.274162 118.812340,91.188713 118.626854,86.153038 C118.464828,81.754181 115.007317,80.781288 111.666672,83.769424 C108.564766,86.544014 105.743721,89.632278 102.646210,92.412163 C98.519287,96.115883 93.187325,96.131935 89.592575,92.650833 C85.800652,88.978790 85.738319,83.501144 89.590172,79.245682 C92.160202,76.406357 95.071365,73.863434 97.508354,70.921532 C98.644897,69.549515 99.968910,67.175354 99.482567,65.928391 C98.952011,64.568085 96.507538,63.409248 94.753471,63.154884 C75.776215,60.402916 59.611244,77.614937 63.590363,96.427399 C64.295799,99.762573 63.735722,101.856697 61.316978,104.234566 C49.672939,115.681763 38.172298,127.276688 26.731220,138.927643 C21.533726,144.220474 21.828087,151.432770 27.139265,156.091965 C32.103416,160.446732 38.381413,159.921982 43.997818,154.388260 C55.510944,143.044617 66.852051,131.525391 78.450829,120.271019 C79.813171,118.949112 82.506157,117.969688 84.314713,118.299362 C98.749039,120.930519 110.081406,115.422195 117.068085,101.337982 z"/>
<path fill="#1CA25B" opacity="1.000000" stroke="none" d=" M92.835266,23.022083 C101.295547,23.213860 102.101974,24.233391 103.089928,35.157913 C108.795174,37.715183 114.297371,40.388798 119.991692,42.558125 C121.124886,42.989830 123.299767,41.842648 124.469460,40.822025 C129.404175,36.516243 132.204132,36.402893 136.923462,40.761639 C137.535004,41.326450 138.097733,41.945801 138.725174,42.491474 C145.816498,48.658714 146.726578,53.194103 139.398239,59.192280 C139.043243,59.482845 138.910278,60.044697 138.827988,60.196339 C141.475266,66.502083 144.032028,72.592224 146.636948,78.797089 C159.118896,80.521065 159.145615,80.552666 158.972443,93.654068 C158.883026,100.417870 156.969391,102.547234 149.986267,102.681595 C146.954834,102.739929 145.799866,103.780693 144.889893,106.644653 C143.289413,111.681892 140.987778,116.496361 138.827591,121.776024 C138.754318,121.575813 138.788422,121.942619 138.985092,122.166840 C146.261215,130.462555 144.141052,139.625412 133.444077,143.586884 C131.190781,144.421356 127.112488,143.031555 125.134735,141.260239 C122.307137,138.727737 120.332664,138.449066 117.423370,140.548538 C115.968300,141.598587 114.208542,142.338699 112.480064,142.876846 C106.998207,144.583542 102.722359,145.855438 101.971764,153.709152 C101.208298,161.697632 92.749634,158.331635 87.371513,158.879456 C83.111946,159.313339 79.451477,157.082016 79.674500,152.950134 C80.020096,146.547058 76.734505,144.623611 71.558319,143.678986 C70.972687,143.572098 70.466492,143.029861 68.821976,141.999115 C73.640266,137.543808 77.882332,133.028183 82.777954,129.392670 C84.679626,127.980476 88.059288,128.253754 90.762108,128.272644 C115.739006,128.447021 133.866669,104.738655 127.042664,80.711487 C121.810631,62.289631 104.170532,51.195923 84.586967,54.011436 C67.390541,56.483757 53.426945,72.595833 53.950626,90.497017 C54.125355,96.469872 52.509956,100.496193 48.130058,104.203537 C45.249161,106.642052 43.044167,109.879097 40.802368,112.453255 C39.436890,111.793068 38.983166,111.714943 38.933784,111.529289 C37.481285,106.068214 35.643330,101.942780 28.351013,102.077286 C23.448399,102.167702 22.170498,97.058365 23.123014,86.367462 C23.503710,82.094612 25.770687,79.140373 30.057184,79.454468 C35.234783,79.833847 36.732948,77.316010 37.935665,72.958702 C39.115070,68.685867 41.382980,64.713478 43.224102,60.492462 C35.819626,49.448124 35.819622,49.448120 45.462185,40.534008 C50.300980,36.060764 51.854774,36.325741 60.079868,42.949146 C65.841438,40.728523 71.550491,38.594177 77.187553,36.284145 C78.030640,35.938652 78.955315,34.853519 79.133850,33.967770 C81.347420,22.985394 81.313591,22.978577 92.835266,23.022083 z"/>
<path fill="#202020" opacity="1.000000" stroke="none" d=" M116.952621,101.710526 C110.081406,115.422195 98.749039,120.930519 84.314713,118.299362 C82.506157,117.969688 79.813171,118.949112 78.450829,120.271019 C66.852051,131.525391 55.510944,143.044617 43.997818,154.388260 C38.381413,159.921982 32.103416,160.446732 27.139265,156.091965 C21.828087,151.432770 21.533726,144.220474 26.731220,138.927643 C38.172298,127.276688 49.672939,115.681763 61.316978,104.234566 C63.735722,101.856697 64.295799,99.762573 63.590363,96.427399 C59.611244,77.614937 75.776215,60.402916 94.753471,63.154884 C96.507538,63.409248 98.952011,64.568085 99.482567,65.928391 C99.968910,67.175354 98.644897,69.549515 97.508354,70.921532 C95.071365,73.863434 92.160202,76.406357 89.590172,79.245682 C85.738319,83.501144 85.800652,88.978790 89.592575,92.650833 C93.187325,96.131935 98.519287,96.115883 102.646210,92.412163 C105.743721,89.632278 108.564766,86.544014 111.666672,83.769424 C115.007317,80.781288 118.464828,81.754181 118.626854,86.153038 C118.812340,91.188713 117.646690,96.274162 116.952621,101.710526 z"/>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="147" height="24" fill="none">
<path
d="M30.827 24h5.224a2.24 2.24 0 0 0 2.241-2.238 2.24 2.24 0 0 0-2.24-2.239h-5.225a2.24 2.24 0 0 0-2.241 2.239A2.24 2.24 0 0 0 30.826 24zM7.465 24H2.241A2.24 2.24 0 0 1 0 21.762a2.24 2.24 0 0 1 2.24-2.239h5.225a2.24 2.24 0 1 1 0 4.477zm13.792-.014h-4.253a2.24 2.24 0 1 1 0-4.477h4.253a2.24 2.24 0 1 1 0 4.477z"
fill="#F23E94" />
<path
d="M28.796 17.558h5.03a2.24 2.24 0 1 0 0-4.477h-5.03a2.24 2.24 0 1 0 0 4.477zm-19.387 0H4.38a2.24 2.24 0 1 1 0-4.477h5.03a2.24 2.24 0 1 1 0 4.477zm13.466-.014h-7.486a2.24 2.24 0 0 1-2.24-2.239 2.24 2.24 0 0 1 2.24-2.239h7.486a2.24 2.24 0 0 1 0 4.477z"
fill="#6927DA" />
<path
d="M22.657 10.92h8.94a2.24 2.24 0 1 0 0-4.477h-8.94a2.24 2.24 0 1 0 0 4.477zm-6.497 0H6.608a2.24 2.24 0 1 1 0-4.477h9.552a2.24 2.24 0 1 1 0 4.477z"
fill="#1570EF" />
<path
d="M29.448 4.477h-5.041a2.24 2.24 0 0 1-2.241-2.238A2.24 2.24 0 0 1 24.407 0h5.041a2.24 2.24 0 1 1 0 4.477zm-15.656 0H8.751A2.24 2.24 0 1 1 8.75 0h5.041a2.24 2.24 0 1 1 0 4.477z"
fill="#2CE" />
<path
d="M73.648 10.81v6.455h-4.49v-5.753c0-1.97-.878-2.632-2.694-2.632-1.698 0-3.142.663-3.142 2.632v5.753h-4.49v-5.753c0-1.97-.878-2.632-2.694-2.632-1.698 0-3.142.663-3.142 2.632v5.753h-4.49v-11.7h4.49v2.067c1.249-1.521 2.889-2.457 5.153-2.457 2.069 0 3.943.858 4.743 3.003 1.308-1.833 3.045-3.003 5.583-3.003 2.772 0 5.173 1.54 5.173 5.635zm13.918 2.945v-.897c-1.503-.215-3.163-.39-4.666-.39-1.971 0-2.713.253-2.713.955 0 .663.586.917 2.264.917 1.406 0 3.826-.37 5.115-.585zm4.49-2.477v5.987h-4.49v-1.56c-2.089 1.287-4.666 1.755-6.657 1.755-3.064 0-5.407-.956-5.407-3.725 0-3.198 3.514-3.997 6.735-3.997 1.99 0 3.806.273 5.329.585-.117-1.619-1.582-2.028-3.631-2.028-2.089 0-4.08.292-6.852.936l-.624-3.062c2.537-.565 5.27-.994 8.315-.994 4.9 0 7.242 1.638 7.281 6.103zm13.976-5.713h4.548l-5.505 11.407c-1.639 3.393-3.416 5.363-7.027 5.363-1.288 0-2.928-.254-4.431-.683V18.24c1.737.39 2.967.585 3.767.585 1.503 0 2.147-.117 2.967-1.56h-2.108l-5.602-11.7h4.548l4.412 9.204 4.431-9.204zm17.744 5.85c0-1.716-1.191-2.535-3.572-2.535-2.382 0-3.573.819-3.573 2.535 0 1.716 1.191 2.535 3.573 2.535 2.381 0 3.572-.82 3.572-2.535zm4.685 0c0 4.719-3.377 6.24-6.403 6.24-2.128 0-4.236-.663-5.427-2.477v2.087h-4.489v-15.6h4.489v5.986c1.191-1.813 3.299-2.476 5.427-2.476 3.026 0 6.403 1.52 6.403 6.24zm6.461-1.482h7.32c-.508-1.346-1.757-1.833-3.69-1.833-1.893 0-3.142.487-3.63 1.833zm7.476 3.14h4.392c-.781 2.905-3.143 4.582-8.062 4.582-5.27 0-8.511-2.106-8.511-6.24s3.455-6.24 8.335-6.24c5.017 0 8.336 2.223 8.336 6.98h-12.123c.313 1.697 1.777 2.185 4.139 2.185 2.206 0 3.104-.507 3.494-1.268z"
fill="#141414" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="128px" viewBox="0 0 256 128" version="1.1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<title>Superset</title>
<g>
<path d="M190.218924,0 C168.269282,0 148.049828,12.3487941 128.508879,33.9252584 C109.307183,12.0095415 88.748476,0 65.7810761,0 C27.7508614,0 0,27.1402067 0,63.67771 C0,100.215213 27.7508614,127.016168 65.7810761,127.016168 C89.1555791,127.016168 107.271667,116.058309 127.491121,94.2104426 C147.03207,116.12616 166.912271,127.084018 190.218924,127.084018 C228.249139,127.016168 256,100.316989 256,63.67771 C256,27.038431 228.249139,0 190.218924,0 Z M66.0524781,88.6806255 C49.9379804,88.6806255 40.3371323,78.0620196 40.3371323,64.0169626 C40.3371323,49.9719056 49.9379804,39.0479724 66.0524781,39.0479724 C79.6225815,39.0479724 90.716141,49.9719056 102.725682,64.6954678 C91.3946462,78.4012722 79.4190299,88.6806255 66.0524781,88.6806255 Z M189.065465,88.6806255 C175.698913,88.6806255 164.401802,78.0620196 152.392261,64.0169626 C164.741055,49.2934005 175.359661,39.0479724 189.065465,39.0479724 C205.179963,39.0479724 214.679035,50.1076067 214.679035,64.0169626 C214.679035,77.9263186 205.179963,88.6806255 189.065465,88.6806255 Z" fill="#484848"></path>
<path d="M156.124039,117.958124 L181.703684,87.4253909 C171.526107,84.3721177 162.12881,75.2122979 152.392261,63.8473363 L127.491121,94.2104426 C135.643361,103.668805 145.322237,111.695521 156.124039,117.958124 Z" fill="#20A7C9"></path>
<path d="M128.508879,33.8913332 C120.41092,24.2972701 110.793109,16.0907501 100.045587,9.60084813 L74.432017,40.4728333 C84.1685661,43.8653591 92.7855818,52.6180758 101.945402,63.7794858 L102.963159,64.4919162 L128.508879,33.8913332 Z" fill="#20A7C9"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,6 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40">
<g id="ss11151339769_1">
<path d="M 0 40 L 0 0 L 40 0 L 40 40 Z" fill="transparent"></path>
<path d="M 34.95 0 L 5.05 0 C 2.262 0 0 2.262 0 5.05 L 0 34.95 C 0 37.738 2.262 40 5.05 40 L 34.95 40 C 37.738 40 40 37.738 40 34.95 L 40 5.05 C 40 2.262 37.738 0 34.95 0 Z M 8.021 14.894 C 8.021 12.709 9.794 10.935 11.979 10.935 L 19.6 10.935 C 19.712 10.935 19.815 11.003 19.862 11.106 C 19.909 11.209 19.888 11.329 19.812 11.415 L 18.141 13.229 C 17.85 13.544 17.441 13.726 17.012 13.726 L 12 13.726 C 11.344 13.726 10.812 14.259 10.812 14.915 L 10.812 17.909 C 10.812 18.294 10.5 18.606 10.115 18.606 L 8.721 18.606 C 8.335 18.606 8.024 18.294 8.024 17.909 L 8.024 14.894 Z M 31.729 25.106 C 31.729 27.291 29.956 29.065 27.771 29.065 L 24.532 29.065 C 22.347 29.065 20.574 27.291 20.574 25.106 L 20.574 19.438 C 20.574 19.053 20.718 18.682 20.979 18.397 L 22.868 16.347 C 22.947 16.262 23.071 16.232 23.182 16.274 C 23.291 16.318 23.365 16.421 23.365 16.538 L 23.365 25.088 C 23.365 25.744 23.897 26.276 24.553 26.276 L 27.753 26.276 C 28.409 26.276 28.941 25.744 28.941 25.088 L 28.941 14.915 C 28.941 14.259 28.409 13.726 27.753 13.726 L 24.032 13.726 C 23.606 13.726 23.2 13.906 22.909 14.218 L 11.812 26.276 L 18.479 26.276 C 18.865 26.276 19.176 26.588 19.176 26.974 L 19.176 28.368 C 19.176 28.753 18.865 29.065 18.479 29.065 L 9.494 29.065 C 8.679 29.065 8.018 28.403 8.018 27.588 L 8.018 26.85 C 8.018 26.479 8.156 26.124 8.409 25.85 L 20.85 12.335 C 21.674 11.441 22.829 10.935 24.044 10.935 L 27.768 10.935 C 29.953 10.935 31.726 12.709 31.726 14.894 L 31.726 25.106 Z" fill="rgb(0,0,0)"></path>
</g>
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2343_96406)">
<path d="M136 2.28882e-05H0L0.000144482 136H136V2.28882e-05ZM27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27ZM107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="black"/>
<path d="M27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27Z" fill="white"/>
<path d="M107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2343_96406">
<rect width="136" height="136" rx="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -9,6 +9,7 @@ import {
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -16,6 +17,7 @@ import {
createMariadb,
createMount,
deployMariadb,
findBackupsByDbId,
findMariadbById,
findProjectById,
findServerById,
@@ -211,8 +213,10 @@ export const mariadbRouter = createTRPCRouter({
});
}
const backups = await findBackupsByDbId(input.mariadbId, "mariadb");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await cancelJobs(backups),
async () => await removeMariadbById(input.mariadbId),
];

View File

@@ -9,6 +9,7 @@ import {
apiSaveExternalPortMongo,
apiUpdateMongo,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -16,6 +17,7 @@ import {
createMongo,
createMount,
deployMongo,
findBackupsByDbId,
findMongoById,
findProjectById,
removeMongoById,
@@ -252,9 +254,11 @@ export const mongoRouter = createTRPCRouter({
message: "You are not authorized to delete this mongo",
});
}
const backups = await findBackupsByDbId(input.mongoId, "mongo");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await cancelJobs(backups),
async () => await removeMongoById(input.mongoId),
];

View File

@@ -12,6 +12,7 @@ import {
import { TRPCError } from "@trpc/server";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -19,6 +20,7 @@ import {
createMount,
createMysql,
deployMySql,
findBackupsByDbId,
findMySqlById,
findProjectById,
removeMySqlById,
@@ -249,8 +251,10 @@ export const mysqlRouter = createTRPCRouter({
});
}
const backups = await findBackupsByDbId(input.mysqlId, "mysql");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
async () => await cancelJobs(backups),
async () => await removeMySqlById(input.mysqlId),
];

View File

@@ -2,20 +2,24 @@ import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiCreateDiscord,
apiCreateEmail,
apiCreateGotify,
apiCreateSlack,
apiCreateTelegram,
apiFindOneNotification,
apiTestDiscordConnection,
apiTestEmailConnection,
apiTestGotifyConnection,
apiTestSlackConnection,
apiTestTelegramConnection,
apiUpdateDiscord,
apiUpdateEmail,
apiUpdateGotify,
apiUpdateSlack,
apiUpdateTelegram,
notifications,
@@ -24,16 +28,19 @@ import {
IS_CLOUD,
createDiscordNotification,
createEmailNotification,
createGotifyNotification,
createSlackNotification,
createTelegramNotification,
findNotificationById,
removeNotificationById,
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
updateDiscordNotification,
updateEmailNotification,
updateGotifyNotification,
updateSlackNotification,
updateTelegramNotification,
} from "@dokploy/server";
@@ -300,10 +307,61 @@ export const notificationRouter = createTRPCRouter({
telegram: true,
discord: true,
email: true,
gotify: true,
},
orderBy: desc(notifications.createdAt),
...(IS_CLOUD && { where: eq(notifications.adminId, ctx.user.adminId) }),
// TODO: Remove this line when the cloud version is ready
});
}),
createGotify: adminProcedure
.input(apiCreateGotify)
.mutation(async ({ input, ctx }) => {
try {
return await createGotifyNotification(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the notification",
cause: error,
});
}
}),
updateGotify: adminProcedure
.input(apiUpdateGotify)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (IS_CLOUD && notification.adminId !== ctx.user.adminId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updateGotifyNotification({
...input,
adminId: ctx.user.adminId,
});
} catch (error) {
throw error;
}
}),
testGotifyConnection: adminProcedure
.input(apiTestGotifyConnection)
.mutation(async ({ input }) => {
try {
await sendGotifyNotification(
input,
"Test Notification",
"Hi, From Dokploy 👋",
);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
cause: error,
});
}
}),
});

View File

@@ -14,6 +14,7 @@ import {
apiSaveExternalPortPostgres,
apiUpdatePostgres,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
import {
IS_CLOUD,
addNewService,
@@ -21,6 +22,7 @@ import {
createMount,
createPostgres,
deployPostgres,
findBackupsByDbId,
findPostgresById,
findProjectById,
removePostgresById,
@@ -231,8 +233,11 @@ export const postgresRouter = createTRPCRouter({
});
}
const backups = await findBackupsByDbId(input.postgresId, "postgres");
const cleanupOperations = [
removeService(postgres.appName, postgres.serverId),
cancelJobs(backups),
removePostgresById(input.postgresId),
];

View File

@@ -68,7 +68,7 @@ export const projectRouter = createTRPCRouter({
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
const { accesedServices } = await findUserByAuthId(ctx.user.authId);
const { accessedServices } = await findUserByAuthId(ctx.user.authId);
await checkProjectAccess(ctx.user.authId, "access", input.projectId);
@@ -79,28 +79,28 @@ export const projectRouter = createTRPCRouter({
),
with: {
compose: {
where: buildServiceFilter(compose.composeId, accesedServices),
where: buildServiceFilter(compose.composeId, accessedServices),
},
applications: {
where: buildServiceFilter(
applications.applicationId,
accesedServices,
accessedServices,
),
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accesedServices),
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accesedServices),
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accesedServices),
where: buildServiceFilter(postgres.postgresId, accessedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accesedServices),
where: buildServiceFilter(redis.redisId, accessedServices),
},
},
});
@@ -125,18 +125,18 @@ export const projectRouter = createTRPCRouter({
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.rol === "user") {
const { accesedProjects, accesedServices } = await findUserByAuthId(
const { accessedProjects, accessedServices } = await findUserByAuthId(
ctx.user.authId,
);
if (accesedProjects.length === 0) {
if (accessedProjects.length === 0) {
return [];
}
const query = await db.query.projects.findMany({
where: and(
sql`${projects.projectId} IN (${sql.join(
accesedProjects.map((projectId) => sql`${projectId}`),
accessedProjects.map((projectId) => sql`${projectId}`),
sql`, `,
)})`,
eq(projects.adminId, ctx.user.adminId),
@@ -145,27 +145,27 @@ export const projectRouter = createTRPCRouter({
applications: {
where: buildServiceFilter(
applications.applicationId,
accesedServices,
accessedServices,
),
with: { domains: true },
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accesedServices),
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accesedServices),
where: buildServiceFilter(mongo.mongoId, accessedServices),
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accesedServices),
where: buildServiceFilter(mysql.mysqlId, accessedServices),
},
postgres: {
where: buildServiceFilter(postgres.postgresId, accesedServices),
where: buildServiceFilter(postgres.postgresId, accessedServices),
},
redis: {
where: buildServiceFilter(redis.redisId, accesedServices),
where: buildServiceFilter(redis.redisId, accessedServices),
},
compose: {
where: buildServiceFilter(compose.composeId, accesedServices),
where: buildServiceFilter(compose.composeId, accessedServices),
with: { domains: true },
},
},
@@ -239,10 +239,13 @@ export const projectRouter = createTRPCRouter({
}
}),
});
function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
return accesedServices.length > 0
function buildServiceFilter(
fieldName: AnyPgColumn,
accessedServices: string[],
) {
return accessedServices.length > 0
? sql`${fieldName} IN (${sql.join(
accesedServices.map((serviceId) => sql`${serviceId}`),
accessedServices.map((serviceId) => sql`${serviceId}`),
sql`, `,
)})`
: sql`1 = 0`;

View File

@@ -244,7 +244,6 @@ export const redisRouter = createTRPCRouter({
message: "You are not authorized to delete this Redis",
});
}
const cleanupOperations = [
async () => await removeService(redis?.appName, redis.serverId),
async () => await removeRedisById(input.redisId),

View File

@@ -345,7 +345,7 @@ export const settingsRouter = createTRPCRouter({
writeConfig("middlewares", input.traefikConfig);
return true;
}),
getUpdateData: adminProcedure.mutation(async () => {
getUpdateData: protectedProcedure.mutation(async () => {
if (IS_CLOUD) {
return DEFAULT_UPDATE_DATA;
}
@@ -373,10 +373,10 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
getDokployVersion: adminProcedure.query(() => {
getDokployVersion: protectedProcedure.query(() => {
return packageInfo.version;
}),
getReleaseTag: adminProcedure.query(() => {
getReleaseTag: protectedProcedure.query(() => {
return getDokployImageTag();
}),
readDirectories: protectedProcedure

View File

@@ -1,3 +1,10 @@
import {
type BackupScheduleList,
IS_CLOUD,
removeScheduleBackup,
scheduleBackup,
} from "@dokploy/server/index";
type QueueJob =
| {
type: "backup";
@@ -59,3 +66,19 @@ export const updateJob = async (job: QueueJob) => {
throw error;
}
};
export const cancelJobs = async (backups: BackupScheduleList) => {
for (const backup of backups) {
if (backup.enabled) {
if (IS_CLOUD) {
await removeJob({
cronSchedule: backup.schedule,
backupId: backup.backupId,
type: "backup",
});
} else {
removeScheduleBackup(backup.backupId);
}
}
}
};

View File

@@ -4,6 +4,7 @@
@layer base {
:root {
--terminal-paste: rgba(0, 0, 0, 0.2);
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
@@ -51,6 +52,7 @@
}
.dark {
--terminal-paste: rgba(255, 255, 255, 0.2);
--background: 0 0% 0%;
--foreground: 0 0% 98%;
@@ -101,9 +103,29 @@
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
/* Custom scrollbar styling */
::-webkit-scrollbar {
width: 0.3125rem;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 0.3125rem;
}
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
}
}
.xterm-viewport {
@@ -215,3 +237,8 @@
background-color: hsl(var(--muted-foreground) / 0.5);
}
}
.xterm-bg-257.xterm-fg-257 {
background-color: var(--terminal-paste) !important;
color: currentColor !important;
}

View File

@@ -0,0 +1,14 @@
version: '3.3'
services:
alist:
image: xhofe/alist:v3.41.0
volumes:
- alist-data:/opt/alist/data
environment:
- PUID=0
- PGID=0
- UMASK=022
restart: unless-stopped
volumes:
alist-data:

View File

@@ -0,0 +1,22 @@
import {
type DomainSchema,
type Schema,
type Template,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 5244,
serviceName: "alist",
},
];
return {
domains,
};
}

View File

@@ -0,0 +1,31 @@
services:
answer:
image: apache/answer:1.4.1
ports:
- '80'
restart: on-failure
volumes:
- answer-data:/data
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- dokploy-network
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: answer
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
answer-data:
db-data:

Some files were not shown because too many files have changed in this diff Show More