Merge pull request #1137 from Dokploy/feat/github-runners

Feat/GitHub runners
This commit is contained in:
Mauricio Siu
2025-01-19 09:58:27 -06:00
committed by GitHub
72 changed files with 18985 additions and 365 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

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

@@ -0,0 +1,134 @@
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

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

@@ -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

@@ -80,7 +80,7 @@ export const ShowContainers = ({ serverId }: Props) => {
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto">
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">

View File

@@ -35,7 +35,7 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto">
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">

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

@@ -3,7 +3,7 @@ import { DockerMonitoring } from "../docker/show";
export const ShowMonitoring = () => {
return (
<div className="my-6 w-full ">
<div className="w-full">
<DockerMonitoring appName="dokploy" />
</div>
);

View File

@@ -114,26 +114,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...."
@@ -238,8 +240,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) => (
@@ -340,6 +342,7 @@ export const AddTemplate = ({ projectId }: Props) => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
size="sm"
className={cn(
"w-auto",

View File

@@ -75,10 +75,10 @@ export const ShowProjects = () => {
list={[{ name: "Projects", href: "/dashboard/projects" }]}
/>
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<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,7 +87,7 @@ export const ShowProjects = () => {
Create and manage your projects
</CardDescription>
</CardHeader>
<div className=" px-4 ">
<div className="">
<HandleProject />
</div>
</div>

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

@@ -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

@@ -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

@@ -70,9 +70,9 @@ export default function SwarmMonitorCard({ serverId }: Props) {
);
return (
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto w-full">
<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" />
@@ -94,7 +94,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
)}
</header>
<div className="grid gap-6 md:grid-cols-3">
<div className="grid gap-6 lg:grid-cols-3">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Nodes</CardTitle>

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>;
};

View File

@@ -1,5 +1,5 @@
"use client";
import { useState, useEffect } from "react";
import {
Activity,
AudioWaveform,
@@ -7,7 +7,9 @@ import {
Bell,
BlocksIcon,
BookIcon,
Boxes,
ChevronRight,
CircleHelp,
Command,
CreditCard,
Database,
@@ -15,7 +17,7 @@ import {
Forward,
GalleryVerticalEnd,
GitBranch,
Heart,
HeartIcon,
KeyRound,
type LucideIcon,
Package,
@@ -44,6 +46,7 @@ import {
import { Separator } from "@/components/ui/separator";
import {
Sidebar,
SIDEBAR_COOKIE_NAME,
SidebarContent,
SidebarFooter,
SidebarGroup,
@@ -85,7 +88,7 @@ interface NavItem {
interface ExternalLink {
name: string;
url: string;
icon: LucideIcon;
icon: React.ComponentType<{ className?: string }>;
}
const data = {
@@ -127,7 +130,7 @@ const data = {
isActive: false,
},
{
title: "File System",
title: "Traefik File System",
url: "/dashboard/traefik",
icon: GalleryVerticalEnd,
isSingle: true,
@@ -210,17 +213,6 @@ const data = {
// url: "/dashboard/settings/notifications",
// },
// ],
// },
// {
// title: "Appearance",
// icon: Frame,
// items: [
// {
// title: "Theme",
// url: "/dashboard/settings/appearance",
// },
// ],
// },
] as NavItem[],
settings: [
{
@@ -288,6 +280,13 @@ const data = {
isSingle: true,
isActive: false,
},
{
title: "Cluster",
url: "/dashboard/settings/cluster",
icon: Boxes,
isSingle: true,
isActive: false,
},
{
title: "Notifications",
url: "/dashboard/settings/notifications",
@@ -302,14 +301,8 @@ const data = {
isSingle: true,
isActive: false,
},
// {
// title: "Appearance",
// url: "/dashboard/settings/appearance",
// icon: Frame,
// },
] as NavItem[],
projects: [
help: [
{
name: "Documentation",
url: "https://docs.dokploy.com/docs/core",
@@ -317,19 +310,21 @@ const data = {
},
{
name: "Support",
url: "https://opencollective.com/dokploy",
icon: Heart,
url: "https://discord.gg/2tBnJ3jDJc",
icon: CircleHelp,
},
{
name: "Sponsor",
url: "https://opencollective.com/dokploy",
icon: ({ className }) => (
<HeartIcon
className={cn(
"text-red-500 fill-red-600 animate-heartbeat",
className,
)}
/>
),
},
// {
// name: "Sales & Marketing",
// url: "#",
// icon: PieChart,
// },
// {
// name: "Travel",
// url: "#",
// icon: Map,
// },
] as ExternalLink[],
};
@@ -348,28 +343,46 @@ function SidebarLogo() {
return (
<Link
href="/dashboard/projects"
className=" flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
className="flex items-center gap-2 p-1 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]/35 rounded-lg "
>
<div
className={cn(
"flex aspect-square items-center justify-center rounded-lg ",
"flex aspect-square items-center justify-center rounded-lg transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
>
<Logo className={state === "collapsed" ? "size-6" : "size-10"} />
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-6" : "size-10",
)}
/>
</div>
{state === "expanded" && (
<div className="flex flex-col gap-1 text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
<span className="truncate font-semibold">Dokploy</span>
<span className="truncate text-xs">{dokployVersion}</span>
</div>
)}
<div className="text-left text-sm leading-tight group-data-[state=open]/collapsible:rotate-90">
<p className="truncate font-semibold">Dokploy</p>
<p className="truncate text-xs text-muted-foreground">
{dokployVersion}
</p>
</div>
</Link>
);
}
export default function Page({ children }: Props) {
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
undefined,
);
useEffect(() => {
const cookieValue = document.cookie
.split("; ")
.find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`))
?.split("=")[1];
setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true");
}, []);
const router = useRouter();
const pathname = usePathname();
const currentPath = router.pathname;
@@ -416,7 +429,11 @@ export default function Page({ children }: Props) {
let filteredSettings = isCloud
? data.settings.filter(
(item) => !["/dashboard/settings/server"].includes(item.url),
(item) =>
![
"/dashboard/settings/server",
"/dashboard/settings/cluster",
].includes(item.url),
)
: data.settings.filter(
(item) => !["/dashboard/settings/billing"].includes(item.url),
@@ -442,6 +459,13 @@ export default function Page({ children }: Props) {
return (
<SidebarProvider
defaultOpen={defaultOpen}
open={defaultOpen}
onOpenChange={(open) => {
setDefaultOpen(open);
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`;
}}
style={
{
"--sidebar-width": "19.5rem",
@@ -451,11 +475,12 @@ export default function Page({ children }: Props) {
>
<Sidebar collapsible="icon" variant="floating">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<LogoWrapper />
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenuButton
className="group-data-[collapsible=icon]:!p-0"
size="lg"
>
<LogoWrapper />
</SidebarMenuButton>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
@@ -627,13 +652,20 @@ export default function Page({ children }: Props) {
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Extra</SidebarGroupLabel>
<SidebarMenu>
{data.projects.map((item) => (
{data.help.map((item: ExternalLink) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<Link href={item.url}>
<item.icon />
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center gap-2"
>
<span className="mr-2">
<item.icon className="h-4 w-4" />
</span>
<span>{item.name}</span>
</Link>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
@@ -665,7 +697,7 @@ export default function Page({ children }: Props) {
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbItem className="block">
<BreadcrumbLink asChild>
<Link
href={activeItem?.url || "/"}
@@ -675,7 +707,7 @@ export default function Page({ children }: Props) {
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbSeparator className="block" />
<BreadcrumbItem>
<BreadcrumbPage>{activeItem?.title}</BreadcrumbPage>
</BreadcrumbItem>

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>
)}
</>
) : (
<>

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";
@@ -231,7 +231,7 @@ const Sidebar = React.forwardRef<
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-out",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
@@ -241,7 +241,7 @@ const Sidebar = React.forwardRef<
/>
<div
className={cn(
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-out md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
@@ -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 @@
ALTER TABLE "admin" ADD COLUMN "enableCleanupCache" boolean DEFAULT true NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" RENAME COLUMN "enableCleanupCache" TO "cleanupCacheOnDeployments";

View File

@@ -0,0 +1,3 @@
ALTER TABLE "admin" RENAME COLUMN "cleanupCacheOnDeployments" TO "cleanupCacheApplications";--> 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;

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,34 @@
"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": 1737266192013,
"tag": "0057_oval_ken_ellis",
"breakpoints": true
},
{
"idx": 58,
"version": "6",
"when": 1737266560950,
"tag": "0058_cultured_warpath",
"breakpoints": true
},
{
"idx": 59,
"version": "6",
"when": 1737268325181,
"tag": "0059_public_speed",
"breakpoints": true
}
]
}

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/", "");

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

@@ -165,10 +165,10 @@ const Mongo = (
router.push(
`/dashboard/project/${data?.projectId}`,
);
toast.success("Postgres deleted successfully");
toast.success("Mongo deleted successfully");
})
.catch(() => {
toast.error("Error deleting the postgres");
toast.error("Error deleting the mongo");
});
}}
>

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

@@ -26,7 +26,7 @@ const Page = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<div className="w-full">
<div className="h-full p-2.5 rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<ProfileForm />
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}

View File

@@ -13,7 +13,7 @@ import superjson from "superjson";
const Page = () => {
return (
<div className="w-full">
<div className="h-full p-2.5 rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<WebDomain />
<WebServer />
</div>

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,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38 38" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M31.079694,0.645206c-1.0388,-0.247509-2.129,-0.177396-3.1268,0.201097
-0.9978,0.378497-1.8564,1.047597-2.4626,1.919097-0.6061,0.8715-0.9314,1.90449
-0.933,2.96277-0.0015,1.05829 0.3208,2.09219 0.9245,2.9654l-0.1325,0.16058
-4.4471,5.23515c-0.2873,-0.1277-0.5906,-0.2311-0.9078,-0.3069-2.7726,-0.6632
-5.563,1.0255-6.2325,3.7716-0.355,1.4559-0.0426,2.9167 0.7424,4.0668l-4.60483,
5.4208c-0.16295,-0.0482-0.32858,-0.087-0.49607,-0.1162-1.21038,-0.2899-2.48522,
-0.1472-3.59979,0.4028-1.11456,0.5501-1.99728,1.4722-2.492545,2.6038-0.495264,
1.1317-0.571271,2.4002-0.214622,3.5819 0.356648,1.1817 1.123057,2.2007 2.164107,
2.8775 1.04105,0.6768 2.2899,0.9678 3.5264,0.8218 1.23651,-0.1461 2.38126,
-0.7198 3.23245,-1.62 0.8512,-0.9003 1.3542,-2.0693 1.4203,-3.3009 0.0661,
-1.2316-0.3089,-2.4468-1.05894,-3.4314l4.29464,-5.1489 0.1792,-0.2109c0.2293,
0.0911 0.468,0.1669 0.7152,0.226 2.7727,0.6632 5.5631,-1.0255 6.2326,-3.7716
0.3373,-1.3835 0.072,-2.7714-0.6289,-3.893l4.6468,-5.4702c0.2538,0.0944 0.5136,
0.1718 0.7778,0.2316 1.3628,0.326 2.8005,0.1023 3.9968,-0.6216 1.1963,-0.72398
2.0533,-1.88899 2.3824,-3.23877 0.3291,-1.34978 0.1033,-2.77376-0.6276,-3.95867
-0.731,-1.18492-1.9072,-2.033711-3.27,-2.359654zM7.630804,34.1959c-0.43554,
-0.1042-0.83012,-0.334-1.13383,-0.6602-0.30371,-0.3263-0.50292,-0.7345
-0.57243,-1.1729-0.0695,-0.4384-0.00619,-0.8874 0.18193,-1.2902 0.18813,
-0.4028 0.49262,-0.7412 0.87497,-0.9726 0.38234,-0.2314 0.82538,-0.3453 1.27308,
-0.3273 0.44769,0.018 0.87994,0.1671 1.24209,0.4285 0.36214,0.2613 0.63791,
0.6231 0.79244,1.0397 0.15453,0.4165 0.18087,0.8691 0.07569,1.3005 -0.14103,
0.5785-0.5083,1.0778-1.02102,1.3881-0.51271,0.3102-1.12887,0.4061-1.71292,
0.2664zM29.307094,7.91571c-0.4355,-0.10417-0.8301,-0.33393-1.1338,-0.66021
-0.3037,-0.32628-0.5029,-0.73444-0.5724,-1.17286-0.0695,-0.43842-0.0062,
-0.88741 0.1819,-1.29018 0.1881,-0.40278 0.4926,-0.74126 0.875,-0.97264
0.3823,-0.23138 0.8253,-0.34527 1.273,-0.32726 0.4477,0.01801 0.88,0.16711
1.2421,0.42844 0.3622,0.26132 0.638,0.62315 0.7925,1.03971 0.1545,0.41656
0.1808,0.86916 0.0757,1.30055 -0.1411,0.57848-0.5083,1.07777-1.0211,1.38804
-0.5127,0.31027-1.1288,0.4061-1.7129,0.26641z"
fill="#0057D9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,37 @@
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="100%"
viewBox="0 0 864 864"
enableBackground="new 0 0 864 864"
xmlSpace="preserve"
>
<path
fill="#EC008C"
opacity="1.000000"
stroke="none"
d="M0.999997,649.000000 C1.000000,433.052795 1.000000,217.105591 1.000000,1.079198 C288.876801,1.079198 576.753601,1.079198 865.000000,1.079198 C865.000000,73.025414 865.000000,145.051453 864.634888,217.500671 C852.362488,223.837280 840.447632,229.735275 828.549438,235.666794 C782.143677,258.801056 735.743225,281.945923 688.998657,304.980469 C688.122009,304.476532 687.580750,304.087708 687.053894,303.680206 C639.556946,266.944733 573.006775,291.446869 560.804199,350.179443 C560.141357,353.369446 559.717590,356.609131 559.195374,359.748962 C474.522705,359.748962 390.283478,359.748962 306.088135,359.748962 C298.804138,318.894806 265.253357,295.206024 231.834442,293.306793 C201.003021,291.554596 169.912033,310.230042 156.935104,338.792725 C149.905151,354.265930 147.884064,370.379944 151.151794,387.034515 C155.204453,407.689667 166.300507,423.954224 183.344437,436.516663 C181.938263,437.607025 180.887405,438.409576 179.849426,439.228516 C147.141953,465.032562 139.918045,510.888947 163.388611,545.322632 C167.274551,551.023804 172.285187,555.958313 176.587341,561.495728 C125.846893,587.012817 75.302292,612.295532 24.735992,637.534790 C16.874903,641.458496 8.914484,645.183228 0.999997,649.000000 z"
/>
<path
fill="#000000"
opacity="1.000000"
stroke="none"
d="M689.340759,305.086823 C735.743225,281.945923 782.143677,258.801056 828.549438,235.666794 C840.447632,229.735275 852.362488,223.837280 864.634888,217.961929 C865.000000,433.613190 865.000000,649.226379 865.000000,864.919800 C577.000000,864.919800 289.000000,864.919800 1.000000,864.919800 C1.000000,793.225708 1.000000,721.576721 0.999997,649.463867 C8.914484,645.183228 16.874903,641.458496 24.735992,637.534790 C75.302292,612.295532 125.846893,587.012817 176.939667,561.513062 C178.543060,562.085083 179.606812,562.886414 180.667526,563.691833 C225.656799,597.853394 291.232574,574.487244 304.462524,519.579773 C304.989105,517.394409 305.501068,515.205505 305.984619,513.166748 C391.466370,513.166748 476.422729,513.166748 561.331177,513.166748 C573.857727,555.764343 608.978149,572.880920 638.519897,572.672791 C671.048340,572.443665 700.623230,551.730408 711.658752,520.910583 C722.546875,490.502106 715.037842,453.265564 682.776733,429.447052 C683.966064,428.506866 685.119507,427.602356 686.265320,426.688232 C712.934143,405.412262 723.011475,370.684631 711.897339,338.686676 C707.312805,325.487671 699.185303,314.725128 689.340759,305.086823 z"
/>
<path
fill="#FEFBFC"
opacity="1.000000"
stroke="none"
d="M688.998657,304.980469 C699.185303,314.725128 707.312805,325.487671 711.897339,338.686676 C723.011475,370.684631 712.934143,405.412262 686.265320,426.688232 C685.119507,427.602356 683.966064,428.506866 682.776733,429.447052 C715.037842,453.265564 722.546875,490.502106 711.658752,520.910583 C700.623230,551.730408 671.048340,572.443665 638.519897,572.672791 C608.978149,572.880920 573.857727,555.764343 561.331177,513.166748 C476.422729,513.166748 391.466370,513.166748 305.984619,513.166748 C305.501068,515.205505 304.989105,517.394409 304.462524,519.579773 C291.232574,574.487244 225.656799,597.853394 180.667526,563.691833 C179.606812,562.886414 178.543060,562.085083 177.128418,561.264465 C172.285187,555.958313 167.274551,551.023804 163.388611,545.322632 C139.918045,510.888947 147.141953,465.032562 179.849426,439.228516 C180.887405,438.409576 181.938263,437.607025 183.344437,436.516663 C166.300507,423.954224 155.204453,407.689667 151.151794,387.034515 C147.884064,370.379944 149.905151,354.265930 156.935104,338.792725 C169.912033,310.230042 201.003021,291.554596 231.834442,293.306793 C265.253357,295.206024 298.804138,318.894806 306.088135,359.748962 C390.283478,359.748962 474.522705,359.748962 559.195374,359.748962 C559.717590,356.609131 560.141357,353.369446 560.804199,350.179443 C573.006775,291.446869 639.556946,266.944733 687.053894,303.680206 C687.580750,304.087708 688.122009,304.476532 688.998657,304.980469 M703.311279,484.370789 C698.954468,457.053253 681.951416,440.229645 656.413696,429.482330 C673.953552,421.977875 688.014709,412.074219 696.456482,395.642365 C704.862061,379.280853 706.487793,362.316345 700.947998,344.809204 C691.688965,315.548492 664.183716,296.954437 633.103516,298.838257 C618.467957,299.725372 605.538086,305.139557 594.588501,314.780121 C577.473999,329.848511 570.185486,349.121399 571.838501,371.750854 C479.166595,371.750854 387.082886,371.750854 294.582672,371.750854 C293.993011,354.662048 288.485260,339.622314 276.940491,327.118439 C265.392609,314.611176 251.082092,307.205322 234.093262,305.960541 C203.355347,303.708374 176.337585,320.898438 166.089890,348.816620 C159.557541,366.613007 160.527206,384.117401 168.756042,401.172516 C177.054779,418.372589 191.471954,428.832886 207.526581,435.632172 C198.407059,442.272583 188.815598,448.302246 180.383728,455.660675 C171.685028,463.251984 166.849655,473.658661 163.940216,484.838684 C161.021744,496.053375 161.212982,507.259705 164.178833,518.426208 C171.577927,546.284302 197.338104,566.588867 226.001465,567.336853 C240.828415,567.723816 254.357819,563.819092 266.385468,555.199646 C284.811554,541.994751 293.631104,523.530579 294.687347,501.238312 C387.354828,501.238312 479.461304,501.238312 571.531799,501.238312 C577.616638,543.189026 615.312866,566.342102 651.310059,559.044739 C684.973938,552.220398 708.263306,519.393127 703.311279,484.370789 z"
/>
<path
fill="#EC008C"
opacity="1.000000"
stroke="none"
d="M703.401855,484.804718 C708.263306,519.393127 684.973938,552.220398 651.310059,559.044739 C615.312866,566.342102 577.616638,543.189026 571.531799,501.238312 C479.461304,501.238312 387.354828,501.238312 294.687347,501.238312 C293.631104,523.530579 284.811554,541.994751 266.385468,555.199646 C254.357819,563.819092 240.828415,567.723816 226.001465,567.336853 C197.338104,566.588867 171.577927,546.284302 164.178833,518.426208 C161.212982,507.259705 161.021744,496.053375 163.940216,484.838684 C166.849655,473.658661 171.685028,463.251984 180.383728,455.660675 C188.815598,448.302246 198.407059,442.272583 207.526581,435.632172 C191.471954,428.832886 177.054779,418.372589 168.756042,401.172516 C160.527206,384.117401 159.557541,366.613007 166.089890,348.816620 C176.337585,320.898438 203.355347,303.708374 234.093262,305.960541 C251.082092,307.205322 265.392609,314.611176 276.940491,327.118439 C288.485260,339.622314 293.993011,354.662048 294.582672,371.750854 C387.082886,371.750854 479.166595,371.750854 571.838501,371.750854 C570.185486,349.121399 577.473999,329.848511 594.588501,314.780121 C605.538086,305.139557 618.467957,299.725372 633.103516,298.838257 C664.183716,296.954437 691.688965,315.548492 700.947998,344.809204 C706.487793,362.316345 704.862061,379.280853 696.456482,395.642365 C688.014709,412.074219 673.953552,421.977875 656.413696,429.482330 C681.951416,440.229645 698.954468,457.053253 703.401855,484.804718 z"
/>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 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

@@ -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

@@ -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

@@ -14,7 +14,7 @@ export const isWSL = async () => {
/** Returns the Docker host IP address. */
export const getDockerHost = async (): Promise<string> => {
if (process.env.NODE_ENV === "production") {
if (process.platform === "linux" && !isWSL()) {
if (process.platform === "linux" && !(await isWSL())) {
try {
// Try to get the Docker bridge IP first
const { stdout } = await execAsync(

View File

@@ -101,9 +101,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 {

View File

@@ -0,0 +1,18 @@
services:
cloudflared:
image: 'cloudflare/cloudflared:latest'
environment:
# Don't forget to set this in your Dokploy Environment
- 'TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}'
network_mode: host
restart: unless-stopped
command: [
"tunnel",
# More tunnel run parameters here:
# https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/tunnel-run-parameters/
"--no-autoupdate",
#"--protocol", "http2",
"run"
]

View File

@@ -0,0 +1,9 @@
import type { Schema, Template } from "../utils";
export function generate(schema: Schema): Template {
const envs = [`CLOUDFLARE_TUNNEL_TOKEN="<INSERT TOKEN>"`];
return {
envs,
};
}

View File

@@ -0,0 +1,48 @@
# conduwuit
# https://conduwuit.puppyirl.gay/deploying/docker-compose.yml
services:
homeserver:
image: girlbossceo/conduwuit:latest
restart: unless-stopped
ports:
- 8448:6167
volumes:
- db:/var/lib/conduwuit
#- ./conduwuit.toml:/etc/conduwuit.toml
environment:
# Edit this in your Dokploy Environment
CONDUWUIT_SERVER_NAME: ${CONDUWUIT_SERVER_NAME}
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
CONDUWUIT_PORT: 6167
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
CONDUWUIT_ALLOW_REGISTRATION: 'true'
CONDUWUIT_REGISTRATION_TOKEN: ${CONDUWUIT_REGISTRATION_TOKEN}
CONDUWUIT_ALLOW_FEDERATION: 'true'
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]'
#CONDUWUIT_LOG: warn,state_res=warn
CONDUWUIT_ADDRESS: 0.0.0.0
# Uncomment if you mapped config toml in volumes
#CONDUWUIT_CONFIG: '/etc/conduwuit.toml'
### Uncomment if you want to use your own Element-Web App.
### Note: You need to provide a config.json for Element and you also need a second
### Domain or Subdomain for the communication between Element and conduwuit
### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md
# element-web:
# image: vectorim/element-web:latest
# restart: unless-stopped
# ports:
# - 8009:80
# volumes:
# - ./element_config.json:/app/config.json
# depends_on:
# - homeserver
volumes:
db:

View File

@@ -0,0 +1,30 @@
import {
type DomainSchema,
type Schema,
type Template,
generatePassword,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const matrixSubdomain = generateRandomDomain(schema);
const registrationToken = generatePassword(20);
const domains: DomainSchema[] = [
{
host: matrixSubdomain,
port: 6167,
serviceName: "homeserver",
},
];
const envs = [
`CONDUWUIT_SERVER_NAME=${matrixSubdomain}`,
`CONDUWUIT_REGISTRATION_TOKEN=${registrationToken}`,
];
return {
envs,
domains,
};
}

View File

@@ -0,0 +1,17 @@
version: '3.8'
services:
couchdb:
image: couchdb:latest
ports:
- '5984'
volumes:
- couchdb-data:/opt/couchdb/data
environment:
- COUCHDB_USER=${COUCHDB_USER}
- COUCHDB_PASSWORD=${COUCHDB_PASSWORD}
restart: unless-stopped
volumes:
couchdb-data:
driver: local

View File

@@ -0,0 +1,28 @@
import {
type DomainSchema,
type Schema,
type Template,
generatePassword,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema);
const username = generatePassword(16);
const password = generatePassword(32);
const domains: DomainSchema[] = [
{
serviceName: "couchdb",
host: mainDomain,
port: 5984,
},
];
const envs = [`COUCHDB_USER=${username}`, `COUCHDB_PASSWORD=${password}`];
return {
envs,
domains,
};
}

View File

@@ -0,0 +1,8 @@
services:
it-tools:
image: corentinth/it-tools:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
interval: 30s
timeout: 10s
retries: 3

View File

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

View File

@@ -1239,4 +1239,63 @@ export const templates: TemplateData[] = [
tags: ["matrix", "communication"],
load: () => import("./conduit/index").then((m) => m.generate),
},
{
id: "conduwuit",
name: "Conduwuit",
version: "latest",
description:
"Well-maintained, featureful Matrix chat homeserver (fork of Conduit)",
logo: "conduwuit.svg",
links: {
github: "https://github.com/girlbossceo/conduwuit",
website: "https://conduwuit.puppyirl.gay",
docs: "https://conduwuit.puppyirl.gay/configuration.html",
},
tags: ["backend", "chat", "communication", "matrix", "server"],
load: () => import("./conduwuit/index").then((m) => m.generate),
},
{
id: "cloudflared",
name: "Cloudflared",
version: "latest",
description:
"A lightweight daemon that securely connects local services to the internet through Cloudflare Tunnel.",
logo: "cloudflared.svg",
links: {
github: "https://github.com/cloudflare/cloudflared",
website:
"https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/",
docs: "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/",
},
tags: ["cloud", "networking", "security", "tunnel"],
load: () => import("./cloudflared/index").then((m) => m.generate),
},
{
id: "couchdb",
name: "CouchDB",
version: "latest",
description:
"CouchDB is a document-oriented NoSQL database that excels at replication and horizontal scaling.",
logo: "couchdb.png", // we defined the name and the extension of the logo
links: {
github: "lorem",
website: "lorem",
docs: "lorem",
},
tags: ["database", "storage"],
load: () => import("./couchdb/index").then((m) => m.generate),
},
{
id: "it-tools",
name: "IT Tools",
version: "latest",
description: "A collection of handy online it-tools for developers.",
logo: "it-tools.svg",
links: {
github: "https://github.com/CorentinTh/it-tools",
website: "https://it-tools.tech",
},
tags: ["developer", "tools"],
load: () => import("./it-tools/index").then((m) => m.generate),
},
];

View File

@@ -31,6 +31,15 @@ export const admins = pgTable("admin", {
stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(true),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
});
export const adminsRelations = relations(admins, ({ one, many }) => ({

View File

@@ -10,6 +10,7 @@ export const notificationType = pgEnum("notificationType", [
"telegram",
"discord",
"email",
"gotify",
]);
export const notifications = pgTable("notification", {
@@ -39,6 +40,9 @@ export const notifications = pgTable("notification", {
emailId: text("emailId").references(() => email.emailId, {
onDelete: "cascade",
}),
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
adminId: text("adminId").references(() => admins.adminId, {
onDelete: "cascade",
}),
@@ -84,6 +88,17 @@ export const email = pgTable("email", {
toAddresses: text("toAddress").array().notNull(),
});
export const gotify = pgTable("gotify", {
gotifyId: text("gotifyId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
serverUrl: text("serverUrl").notNull(),
appToken: text("appToken").notNull(),
priority: integer("priority").notNull().default(5),
decoration: boolean("decoration"),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -101,6 +116,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.emailId],
references: [email.emailId],
}),
gotify: one(gotify, {
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
admin: one(admins, {
fields: [notifications.adminId],
references: [admins.adminId],
@@ -224,6 +243,39 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
fromAddress: true,
});
export const apiCreateGotify = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
})
.extend({
serverUrl: z.string().min(1),
appToken: z.string().min(1),
priority: z.number().min(1),
decoration: z.boolean(),
})
.required();
export const apiUpdateGotify = apiCreateGotify.partial().extend({
notificationId: z.string().min(1),
gotifyId: z.string().min(1),
adminId: z.string().optional(),
});
export const apiTestGotifyConnection = apiCreateGotify
.pick({
serverUrl: true,
appToken: true,
priority: true,
})
.extend({
decoration: z.boolean().optional(),
});
export const apiFindOneNotification = notificationsSchema
.pick({
notificationId: true,
@@ -242,5 +294,8 @@ export const apiSendTest = notificationsSchema
username: z.string(),
password: z.string(),
toAddresses: z.array(z.string()),
serverUrl: z.string(),
appToken: z.string(),
priority: z.number(),
})
.partial();

View File

@@ -40,11 +40,11 @@ export const users = pgTable("user", {
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
accesedProjects: text("accesedProjects")
accessedProjects: text("accesedProjects")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accesedServices: text("accesedServices")
accessedServices: text("accesedServices")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
@@ -73,8 +73,8 @@ const createSchema = createInsertSchema(users, {
token: z.string().min(1),
isRegistered: z.boolean().optional(),
adminId: z.string(),
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(),
canCreateServices: z.boolean().optional(),
canDeleteProjects: z.boolean().optional(),
@@ -106,8 +106,8 @@ export const apiAssignPermissions = createSchema
canCreateServices: true,
canDeleteProjects: true,
canDeleteServices: true,
accesedProjects: true,
accesedServices: true,
accessedProjects: true,
accessedServices: true,
canAccessToTraefikFiles: true,
canAccessToDocker: true,
canAccessToAPI: true,

View File

@@ -40,7 +40,7 @@ import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin";
import { findAdminById, getDokployUrl } from "./admin";
import {
createDeployment,
createDeploymentPreview,
@@ -58,6 +58,7 @@ import {
updatePreviewDeployment,
} from "./preview-deployment";
import { validUniqueServerAppName } from "./project";
import { cleanupFullDocker } from "./settings";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -213,6 +214,7 @@ export const deployApplication = async ({
applicationType: "application",
buildLink,
adminId: application.project.adminId,
domains: application.domains,
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -228,6 +230,12 @@ export const deployApplication = async ({
});
throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
}
return true;
@@ -269,6 +277,12 @@ export const rebuildApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
}
return true;
@@ -332,6 +346,7 @@ export const deployRemoteApplication = async ({
applicationType: "application",
buildLink,
adminId: application.project.adminId,
domains: application.domains,
});
} catch (error) {
// @ts-ignore
@@ -357,15 +372,13 @@ export const deployRemoteApplication = async ({
adminId: application.project.adminId,
});
console.log(
"Error on ",
application.buildType,
"/",
application.sourceType,
error,
);
throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
}
return true;
@@ -473,6 +486,12 @@ export const deployPreviewApplication = async ({
previewStatus: "error",
});
throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheOnPreviews) {
await cleanupFullDocker(application?.serverId);
}
}
return true;
@@ -585,6 +604,12 @@ export const deployRemotePreviewApplication = async ({
previewStatus: "error",
});
throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheOnPreviews) {
await cleanupFullDocker(application?.serverId);
}
}
return true;
@@ -632,6 +657,12 @@ export const rebuildRemoteApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
}
return true;

View File

@@ -3,7 +3,6 @@ import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import {
buildCompose,
getBuildComposeCommand,
@@ -45,9 +44,10 @@ import {
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin";
import { findAdminById, getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import { validUniqueServerAppName } from "./project";
import { cleanupFullDocker } from "./settings";
export type Compose = typeof compose.$inferSelect;
@@ -243,6 +243,7 @@ export const deployCompose = async ({
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
domains: compose.domains,
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -259,6 +260,11 @@ export const deployCompose = async ({
adminId: compose.project.adminId,
});
throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
}
};
@@ -295,6 +301,11 @@ export const rebuildCompose = async ({
composeStatus: "error",
});
throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
}
return true;
@@ -366,6 +377,7 @@ export const deployRemoteCompose = async ({
applicationType: "compose",
buildLink,
adminId: compose.project.adminId,
domains: compose.domains,
});
} catch (error) {
// @ts-ignore
@@ -392,6 +404,11 @@ export const deployRemoteCompose = async ({
adminId: compose.project.adminId,
});
throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
}
};
@@ -436,6 +453,11 @@ export const rebuildRemoteCompose = async ({
composeStatus: "error",
});
throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
}
return true;

View File

@@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateGotify,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateGotify,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
gotify,
notifications,
slack,
telegram,
@@ -379,6 +382,96 @@ export const updateEmailNotification = async (
});
};
export const createGotifyNotification = async (
input: typeof apiCreateGotify._type,
adminId: string,
) => {
await db.transaction(async (tx) => {
const newGotify = await tx
.insert(gotify)
.values({
serverUrl: input.serverUrl,
appToken: input.appToken,
priority: input.priority,
decoration: input.decoration,
})
.returning()
.then((value) => value[0]);
if (!newGotify) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting gotify",
});
}
const newDestination = await tx
.insert(notifications)
.values({
gotifyId: newGotify.gotifyId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "gotify",
adminId: adminId,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateGotifyNotification = async (
input: typeof apiUpdateGotify._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
adminId: input.adminId,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(gotify)
.set({
serverUrl: input.serverUrl,
appToken: input.appToken,
priority: input.priority,
decoration: input.decoration,
})
.where(eq(gotify.gotifyId, input.gotifyId));
return newDestination;
});
};
export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId),
@@ -387,6 +480,7 @@ export const findNotificationById = async (notificationId: string) => {
telegram: true,
discord: true,
email: true,
gotify: true,
},
});
if (!notification) {

View File

@@ -5,6 +5,7 @@ import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { findAdminById } from "./admin";
// import packageInfo from "../../../package.json";
export interface IUpdateData {
@@ -213,3 +214,35 @@ echo "$json_output"
}
return result;
};
export const cleanupFullDocker = async (serverId?: string | null) => {
const cleanupImages = "docker image prune --all --force";
const cleanupVolumes = "docker volume prune --all --force";
const cleanupContainers = "docker container prune --force";
const cleanupSystem = "docker system prune --all --force --volumes";
const cleanupBuilder = "docker builder prune --all --force";
try {
if (serverId) {
await execAsyncRemote(
serverId,
`
${cleanupImages}
${cleanupVolumes}
${cleanupContainers}
${cleanupSystem}
${cleanupBuilder}
`,
);
}
await execAsync(`
${cleanupImages}
${cleanupVolumes}
${cleanupContainers}
${cleanupSystem}
${cleanupBuilder}
`);
} catch (error) {
console.log(error);
}
};

View File

@@ -54,7 +54,7 @@ export const addNewProject = async (authId: string, projectId: string) => {
await db
.update(users)
.set({
accesedProjects: [...user.accesedProjects, projectId],
accessedProjects: [...user.accessedProjects, projectId],
})
.where(eq(users.authId, authId));
};
@@ -64,7 +64,7 @@ export const addNewService = async (authId: string, serviceId: string) => {
await db
.update(users)
.set({
accesedServices: [...user.accesedServices, serviceId],
accessedServices: [...user.accessedServices, serviceId],
})
.where(eq(users.authId, authId));
};
@@ -73,8 +73,9 @@ export const canPerformCreationService = async (
userId: string,
projectId: string,
) => {
const { accesedProjects, canCreateServices } = await findUserByAuthId(userId);
const haveAccessToProject = accesedProjects.includes(projectId);
const { accessedProjects, canCreateServices } =
await findUserByAuthId(userId);
const haveAccessToProject = accessedProjects.includes(projectId);
if (canCreateServices && haveAccessToProject) {
return true;
@@ -87,8 +88,8 @@ export const canPerformAccessService = async (
userId: string,
serviceId: string,
) => {
const { accesedServices } = await findUserByAuthId(userId);
const haveAccessToService = accesedServices.includes(serviceId);
const { accessedServices } = await findUserByAuthId(userId);
const haveAccessToService = accessedServices.includes(serviceId);
if (haveAccessToService) {
return true;
@@ -101,8 +102,9 @@ export const canPeformDeleteService = async (
authId: string,
serviceId: string,
) => {
const { accesedServices, canDeleteServices } = await findUserByAuthId(authId);
const haveAccessToService = accesedServices.includes(serviceId);
const { accessedServices, canDeleteServices } =
await findUserByAuthId(authId);
const haveAccessToService = accessedServices.includes(serviceId);
if (canDeleteServices && haveAccessToService) {
return true;
@@ -135,9 +137,9 @@ export const canPerformAccessProject = async (
authId: string,
projectId: string,
) => {
const { accesedProjects } = await findUserByAuthId(authId);
const { accessedProjects } = await findUserByAuthId(authId);
const haveAccessToProject = accesedProjects.includes(projectId);
const haveAccessToProject = accessedProjects.includes(projectId);
if (haveAccessToProject) {
return true;

View File

@@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed";
import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -39,11 +41,12 @@ export const sendBuildErrorNotifications = async ({
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
@@ -112,22 +115,35 @@ export const sendBuildErrorNotifications = async ({
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("⚠️", "Build Failed"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
await sendTelegramNotification(
telegram,
`
<b>⚠️ Build Failed</b>
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${applicationType}
<b>Time:</b> ${date.toLocaleString()}
<b>Error:</b>
<pre>${errorMessage}</pre>
<b>Build Details:</b> ${buildLink}
`,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton,
);
}

View File

@@ -1,11 +1,14 @@
import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success";
import type { Domain } from "@dokploy/server/services/domain";
import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -16,6 +19,7 @@ interface Props {
applicationType: string;
buildLink: string;
adminId: string;
domains: Domain[];
}
export const sendBuildSuccessNotifications = async ({
@@ -24,6 +28,7 @@ export const sendBuildSuccessNotifications = async ({
applicationType,
buildLink,
adminId,
domains,
}: Props) => {
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
@@ -37,11 +42,12 @@ export const sendBuildSuccessNotifications = async ({
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -106,19 +112,45 @@ export const sendBuildSuccessNotifications = async ({
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Build Success"),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
array.slice(i * chunkSize, i * chunkSize + chunkSize),
);
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
...chunkArray(domains, 2).map((chunk) =>
chunk.map((data) => ({
text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`,
})),
),
];
await sendTelegramNotification(
telegram,
`
<b>✅ Build Success</b>
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${applicationType}
<b>Time:</b> ${date.toLocaleString()}
<b>Build Details:</b> ${buildLink}
`,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton,
);
}

View File

@@ -1,11 +1,14 @@
import { error } from "node:console";
import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup";
import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -37,11 +40,12 @@ export const sendDatabaseBackupNotifications = async ({
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -120,19 +124,35 @@ export const sendDatabaseBackupNotifications = async ({
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("❔", `Type: ${databaseType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (telegram) {
const isError = type === "error" && errorMessage;
const statusEmoji = type === "success" ? "✅" : "❌";
const messageText = `
<b>${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}</b>
const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg = isError
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
: "";
<b>Project:</b> ${projectName}
<b>Application:</b> ${applicationName}
<b>Type:</b> ${databaseType}
<b>Time:</b> ${date.toLocaleString()}
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
<b>Status:</b> ${type === "success" ? "Successful" : "Failed"}
${type === "error" && errorMessage ? `<b>Error:</b> ${errorMessage}` : ""}
`;
await sendTelegramNotification(telegram, messageText);
}

View File

@@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup";
import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -26,11 +28,12 @@ export const sendDockerCleanupNotifications = async (
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -79,14 +82,21 @@ export const sendDockerCleanupNotifications = async (
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Docker Cleanup"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("📜", `Message:\n${message}`)}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`
<b>✅ Docker Cleanup</b>
<b>Message:</b> ${message}
<b>Time:</b> ${date.toLocaleString()}
`,
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
}

View File

@@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart";
import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -20,11 +22,12 @@ export const sendDokployRestartNotifications = async () => {
discord: true,
telegram: true,
slack: true,
gotify: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack } = notification;
const { email, discord, telegram, slack, gotify } = notification;
if (email) {
const template = await renderAsync(
@@ -64,13 +67,20 @@ export const sendDokployRestartNotifications = async () => {
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`
<b>✅ Dokploy Serverd Restarted</b>
<b>Time:</b> ${date.toLocaleString()}
`,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
}

View File

@@ -1,6 +1,7 @@
import type {
discord,
email,
gotify,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -55,6 +56,10 @@ export const sendDiscordNotification = async (
export const sendTelegramNotification = async (
connection: typeof telegram.$inferInsert,
messageText: string,
inlineButton?: {
text: string;
url: string;
}[][],
) => {
try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
@@ -66,6 +71,9 @@ export const sendTelegramNotification = async (
text: messageText,
parse_mode: "HTML",
disable_web_page_preview: true,
reply_markup: {
inline_keyboard: inlineButton,
},
}),
});
} catch (err) {
@@ -87,3 +95,33 @@ export const sendSlackNotification = async (
console.log(err);
}
};
export const sendGotifyNotification = async (
connection: typeof gotify.$inferInsert,
title: string,
message: string,
) => {
const response = await fetch(`${connection.serverUrl}/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Gotify-Key": connection.appToken,
},
body: JSON.stringify({
title: title,
message: message,
priority: connection.priority,
extras: {
"client::display": {
contentType: "text/plain",
},
},
}),
});
if (!response.ok) {
throw new Error(
`Failed to send Gotify notification: ${response.statusText}`,
);
}
};