Compare commits

..

2 Commits

Author SHA1 Message Date
Mauricio Siu
0dbd1039e8 feat: enhance auth schema and add CLI configuration
- Updated the user schema to include new fields: banned, banReason, and banExpires for improved user management.
- Introduced a new auth-cli configuration file to facilitate database schema generation and inspection for better-auth plugins.
- Ensured the CLI configuration mirrors the plugin set in auth.ts for consistency across the application.
2026-04-19 12:05:37 -06:00
Mauricio Siu
f06c9deddf feat: implement SCIM provisioning support
- Updated dependencies for better-auth packages to version 1.6.5, including api-key, sso, and utils.
- Introduced a new SCIM dialog component for managing SCIM providers and tokens.
- Added SCIM provider management functionality in the backend, including listing, generating tokens, and deleting providers.
- Created a new database table for SCIM providers and updated the schema accordingly.
- Enhanced authentication logic to support SCIM provisioning with enterprise features validation.
2026-04-19 11:59:56 -06:00
164 changed files with 1897 additions and 2798 deletions

View File

@@ -138,8 +138,6 @@ jobs:
needs: [combine-manifests] needs: [combine-manifests]
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -162,80 +160,3 @@ jobs:
prerelease: false prerelease: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
sync-version:
needs: [generate-release]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Sync version to MCP repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo
cd /tmp/mcp-repo
jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
npm install -g pnpm
pnpm install
pnpm run fetch-openapi
pnpm run generate
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
--allow-empty
git push
echo "✅ MCP repo synced to version ${{ needs.generate-release.outputs.version }}"
- name: Sync version to CLI repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo
cd /tmp/cli-repo
jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
cp ${{ github.workspace }}/openapi.json ./openapi.json
npm install -g pnpm
pnpm install
pnpm run generate
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
--allow-empty
git push
echo "✅ CLI repo synced to version ${{ needs.generate-release.outputs.version }}"
- name: Sync version to SDK repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git /tmp/sdk-repo
cd /tmp/sdk-repo
jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
cp ${{ github.workspace }}/openapi.json ./openapi.json
npm install -g pnpm
pnpm install
pnpm run generate
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ needs.generate-release.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
--allow-empty
git push
echo "✅ SDK repo synced to version ${{ needs.generate-release.outputs.version }}"

View File

@@ -110,24 +110,3 @@ jobs:
echo "✅ OpenAPI synced to CLI repository successfully" echo "✅ OpenAPI synced to CLI repository successfully"
- name: Sync to SDK repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git sdk-repo
cd sdk-repo
cp -f ../openapi.json openapi.json
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add openapi.json
git commit -m "chore: sync OpenAPI specification [skip ci]" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
--allow-empty
git push
echo "✅ OpenAPI synced to SDK repository successfully"

80
.github/workflows/sync-version.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Sync version to MCP and CLI repos
on:
release:
types: [published]
workflow_dispatch:
jobs:
sync-version:
name: Sync version to external repos
runs-on: ubuntu-latest
steps:
- name: Checkout Dokploy repository
uses: actions/checkout@v4
- name: Get version
id: get_version
run: |
VERSION=$(jq -r .version apps/dokploy/package.json | sed 's/^v//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Sync version to MCP repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo
cd /tmp/mcp-repo
# Regenerate tools from latest OpenAPI spec
npm install -g pnpm
pnpm install
pnpm run fetch-openapi
pnpm run generate
# Bump version after install so pnpm install doesn't overwrite it
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Release: ${{ github.event.release.html_url }}" \
--allow-empty
git push
- name: Sync version to CLI repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo
cd /tmp/cli-repo
# Copy latest openapi spec and regenerate commands
cp ${{ github.workspace }}/openapi.json ./openapi.json
npm install -g pnpm
pnpm install
pnpm run generate
# Bump version after install so pnpm install doesn't overwrite it
if [ -f package.json ]; then
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
fi
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Release: ${{ github.event.release.html_url }}" \
--allow-empty
git push
echo "CLI repo synced to version ${{ steps.get_version.outputs.version }}"

View File

@@ -4,8 +4,5 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit", "source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit" "source.organizeImports.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
} }
} }

View File

@@ -66,7 +66,7 @@ COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \ HEALTHCHECK --interval=10s --timeout=3s --retries=10 \
CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1 CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1
CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"] CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]

View File

@@ -1,41 +0,0 @@
import { shouldDeploy } from "@dokploy/server";
import { describe, expect, it } from "vitest";
describe("shouldDeploy", () => {
it("should deploy when no watch paths are configured", () => {
expect(shouldDeploy(null, ["src/index.ts"])).toBe(true);
expect(shouldDeploy([], ["src/index.ts"])).toBe(true);
});
it("should deploy when watch paths match modified files", () => {
expect(shouldDeploy(["src/**"], ["src/index.ts"])).toBe(true);
expect(shouldDeploy(["apps/web/**"], ["apps/web/page.tsx"])).toBe(true);
});
it("should not deploy when watch paths do not match", () => {
expect(shouldDeploy(["src/**"], ["docs/readme.md"])).toBe(false);
});
it("should not throw when modified files contain non-string values", () => {
expect(() =>
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
).not.toThrow();
expect(
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
).toBe(true);
});
it("should not throw when modified files are undefined or null", () => {
expect(() => shouldDeploy(["src/**"], undefined)).not.toThrow();
expect(() => shouldDeploy(["src/**"], null)).not.toThrow();
expect(shouldDeploy(["src/**"], undefined)).toBe(false);
expect(shouldDeploy(["src/**"], null)).toBe(false);
});
it("should not throw when every modified file is non-string", () => {
expect(() =>
shouldDeploy(["src/**"], [undefined, undefined] as any),
).not.toThrow();
expect(shouldDeploy(["src/**"], [undefined, undefined] as any)).toBe(false);
});
});

View File

@@ -494,49 +494,4 @@ describe("processTemplate", () => {
expect(result.mounts).toHaveLength(1); expect(result.mounts).toHaveLength(1);
}); });
}); });
describe("isolated deployment config", () => {
it("should default to isolated=true when not specified", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: {},
},
};
expect(template.config.isolated).toBeUndefined();
// undefined !== false => isolatedDeployment = true
expect(template.config.isolated !== false).toBe(true);
});
it("should be isolated when isolated=true is explicitly set", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
isolated: true,
domains: [],
env: {},
},
};
expect(template.config.isolated !== false).toBe(true);
});
it("should disable isolated deployment when isolated=false", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
isolated: false,
domains: [],
env: {},
},
};
expect(template.config.isolated !== false).toBe(false);
});
});
}); });

View File

@@ -30,7 +30,9 @@ describe("helpers functions", () => {
const domain = processValue("${domain}", {}, mockSchema); const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect( expect(
domain.endsWith(`${mockSchema.serverIp.replaceAll(".", "-")}.sslip.io`), domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy(); ).toBeTruthy();
}); });
}); });

View File

@@ -78,20 +78,4 @@ describe("readValidDirectory (path traversal)", () => {
it("returns false for empty string (resolves to cwd)", () => { it("returns false for empty string (resolves to cwd)", () => {
expect(readValidDirectory("")).toBe(false); expect(readValidDirectory("")).toBe(false);
}); });
it("returns true for Next.js dynamic route paths with square brackets", () => {
expect(
readValidDirectory(
`${BASE}/applications/myapp/code/app/api/[id]/route.ts`,
),
).toBe(true);
expect(
readValidDirectory(`${BASE}/applications/myapp/code/pages/[slug].tsx`),
).toBe(true);
expect(
readValidDirectory(
`${BASE}/applications/myapp/code/app/[...catch]/page.tsx`,
),
).toBe(true);
});
}); });

View File

@@ -21,9 +21,9 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import type { RouterOutputs } from "@/utils/api"; import type { RouterOutputs } from "@/utils/api";
import { DnsHelperModal } from "./dns-helper-modal";
import { AddDomain } from "./handle-domain";
import type { ValidationStates } from "./show-domains"; import type { ValidationStates } from "./show-domains";
import { AddDomain } from "./handle-domain";
import { DnsHelperModal } from "./dns-helper-modal";
export type Domain = export type Domain =
| RouterOutputs["domain"]["byApplicationId"][0] | RouterOutputs["domain"]["byApplicationId"][0]
@@ -168,7 +168,7 @@ export const createColumns = ({
{domain.certificateType} {domain.certificateType}
</Badge> </Badge>
)} )}
{!domain.host.includes("sslip.io") && ( {!domain.host.includes("traefik.me") && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -256,7 +256,7 @@ export const createColumns = ({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!domain.host.includes("sslip.io") && ( {!domain.host.includes("traefik.me") && (
<DnsHelperModal <DnsHelperModal
domain={{ domain={{
host: domain.host, host: domain.host,

View File

@@ -225,7 +225,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const https = form.watch("https"); const https = form.watch("https");
const domainType = form.watch("domainType"); const domainType = form.watch("domainType");
const host = form.watch("host"); const host = form.watch("host");
const isTraefikMeDomain = host?.includes("sslip.io") || false; const isTraefikMeDomain = host?.includes("traefik.me") || false;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -513,7 +513,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
{!canGenerateTraefikMeDomains && {!canGenerateTraefikMeDomains &&
field.value.includes("sslip.io") && ( field.value.includes("traefik.me") && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You need to set an IP address in your{" "} You need to set an IP address in your{" "}
<Link <Link
@@ -524,12 +524,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
? "Remote Servers -> Server -> Edit Server -> Update IP Address" ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"} : "Web Server -> Server -> Update Server IP"}
</Link>{" "} </Link>{" "}
to make your sslip.io domain work. to make your traefik.me domain work.
</AlertBlock> </AlertBlock>
)} )}
{isTraefikMeDomain && ( {isTraefikMeDomain && (
<AlertBlock type="info"> <AlertBlock type="info">
<strong>Note:</strong> sslip.io is a public HTTP <strong>Note:</strong> traefik.me is a public HTTP
service and does not support SSL/HTTPS. HTTPS and service and does not support SSL/HTTPS. HTTPS and
certificate options will not have any effect. certificate options will not have any effect.
</AlertBlock> </AlertBlock>
@@ -567,7 +567,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-[10rem]"
> >
<p>Generate sslip.io domain</p> <p>Generate traefik.me domain</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View File

@@ -425,7 +425,7 @@ export const ShowDomains = ({ id, type }: Props) => {
</Badge> </Badge>
)} )}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{!item.host.includes("sslip.io") && ( {!item.host.includes("traefik.me") && (
<DnsHelperModal <DnsHelperModal
domain={{ domain={{
host: item.host, host: item.host,

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(), slug: z.string().optional(),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(), enableSubmodules: z.boolean().optional(),

View File

@@ -6,7 +6,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitIcon } from "@/components/icons/data-tools-icons"; import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -42,10 +41,7 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, { repositoryURL: z.string().min(1, {
message: "Repository URL is required", message: "Repository URL is required",
}), }),
branch: z branch: z.string().min(1, "Branch required"),
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),
@@ -111,103 +107,110 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 items-start"> onSubmit={form.handleSubmit(onSubmit)}
<FormField className="flex flex-col gap-4"
control={form.control} >
name="repositoryURL" <div className="grid md:grid-cols-2 gap-4">
render={({ field }) => ( <div className="flex items-end col-span-2 gap-4">
<FormItem className="col-span-2 lg:col-span-3"> <div className="grow">
<div className="flex items-center justify-between h-5"> <FormField
<FormLabel>Repository URL</FormLabel> control={form.control}
{field.value?.startsWith("https://") && ( name="repositoryURL"
<Link render={({ field }) => (
href={field.value} <FormItem>
target="_blank" <div className="flex items-center justify-between">
rel="noopener noreferrer" <FormLabel>Repository URL</FormLabel>
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" {field.value?.startsWith("https://") && (
> <Link
<GitIcon className="h-4 w-4" /> href={field.value}
<span>View Repository</span> target="_blank"
</Link> rel="noopener noreferrer"
)} className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
</div> >
<FormControl> <GitIcon className="h-4 w-4" />
<Input placeholder="Repository URL" {...field} /> <span>View Repository</span>
</FormControl> </Link>
<FormMessage /> )}
</FormItem> </div>
<FormControl>
<Input placeholder="Repository URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{sshKeys && sshKeys.length > 0 ? (
<FormField
control={form.control}
name="sshKey"
render={({ field }) => (
<FormItem className="basis-40">
<FormLabel className="w-full inline-flex justify-between">
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl>
<Select
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
) : (
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
)} )}
/> </div>
{sshKeys && sshKeys.length > 0 ? ( <div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="sshKey" name="branch"
render={({ field }) => ( render={({ field }) => (
<FormItem className="col-span-2 lg:col-span-1"> <FormItem>
<FormLabel className="w-full inline-flex justify-between"> <FormLabel>Branch</FormLabel>
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl> <FormControl>
<Select <Input placeholder="Branch" {...field} />
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormControl> </FormControl>
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
) : ( </div>
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
className="col-span-2 lg:col-span-1 lg:mt-7"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
)}
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Branch</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="buildPath" name="buildPath"
render={({ field }) => ( render={({ field }) => (
<FormItem className="col-span-2"> <FormItem>
<FormLabel>Build Path</FormLabel> <FormLabel>Build Path</FormLabel>
<FormControl> <FormControl>
<Input placeholder="/" {...field} /> <Input placeholder="/" {...field} />
@@ -220,7 +223,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="watchPaths" name="watchPaths"
render={({ field }) => ( render={({ field }) => (
<FormItem className="col-span-2 lg:col-span-4"> <FormItem className="md:col-span-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel> <FormLabel>Watch Paths</FormLabel>
<TooltipProvider> <TooltipProvider>

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -73,10 +72,7 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]), watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(), enableSubmodules: z.boolean().optional(),

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"), triggerType: z.enum(["push", "tag"]).default("push"),

View File

@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
id: z.number().nullable(), id: z.number().nullable(),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -58,7 +58,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle> <CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-2 lg:flex lg:flex-row lg:flex-wrap gap-4"> <CardContent className="flex flex-row gap-4 flex-wrap">
<TooltipProvider delayDuration={0} disableHoverableContent={false}> <TooltipProvider delayDuration={0} disableHoverableContent={false}>
{canDeploy && ( {canDeploy && (
<DialogAction <DialogAction
@@ -274,14 +274,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
> >
<Button <Button
variant="outline" variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2 col-span-2" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
> >
<Terminal className="size-4 mr-1" /> <Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
{canUpdateService && ( {canUpdateService && (
<div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1"> <div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span> <span className="text-sm font-medium">Autodeploy</span>
<Switch <Switch
aria-label="Toggle autodeploy" aria-label="Toggle autodeploy"
@@ -305,7 +305,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
)} )}
{canUpdateService && ( {canUpdateService && (
<div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1"> <div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Clean Cache</span> <span className="text-sm font-medium">Clean Cache</span>
<Switch <Switch
aria-label="Toggle clean cache" aria-label="Toggle clean cache"

View File

@@ -87,7 +87,7 @@ export const AddPreviewDomain = ({
}); });
const host = form.watch("host"); const host = form.watch("host");
const isTraefikMeDomain = host?.includes("sslip.io") || false; const isTraefikMeDomain = host?.includes("traefik.me") || false;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -162,7 +162,7 @@ export const AddPreviewDomain = ({
<FormItem> <FormItem>
{isTraefikMeDomain && ( {isTraefikMeDomain && (
<AlertBlock type="info"> <AlertBlock type="info">
<strong>Note:</strong> sslip.io is a public HTTP <strong>Note:</strong> traefik.me is a public HTTP
service and does not support SSL/HTTPS. HTTPS and service and does not support SSL/HTTPS. HTTPS and
certificate options will not have any effect. certificate options will not have any effect.
</AlertBlock> </AlertBlock>
@@ -202,7 +202,7 @@ export const AddPreviewDomain = ({
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-[10rem]"
> >
<p>Generate sslip.io domain</p> <p>Generate traefik.me domain</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View File

@@ -88,7 +88,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
env: "", env: "",
wildcardDomain: "*.sslip.io", wildcardDomain: "*.traefik.me",
port: 3000, port: 3000,
previewLimit: 3, previewLimit: 3,
previewLabels: [], previewLabels: [],
@@ -102,7 +102,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
const previewHttps = form.watch("previewHttps"); const previewHttps = form.watch("previewHttps");
const wildcardDomain = form.watch("wildcardDomain"); const wildcardDomain = form.watch("wildcardDomain");
const isTraefikMeDomain = wildcardDomain?.includes("sslip.io") || false; const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false;
useEffect(() => { useEffect(() => {
setIsEnabled(data?.isPreviewDeploymentsActive || false); setIsEnabled(data?.isPreviewDeploymentsActive || false);
@@ -114,7 +114,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
env: data.previewEnv || "", env: data.previewEnv || "",
buildArgs: data.previewBuildArgs || "", buildArgs: data.previewBuildArgs || "",
buildSecrets: data.previewBuildSecrets || "", buildSecrets: data.previewBuildSecrets || "",
wildcardDomain: data.previewWildcard || "*.sslip.io", wildcardDomain: data.previewWildcard || "*.traefik.me",
port: data.previewPort || 3000, port: data.previewPort || 3000,
previewLabels: data.previewLabels || [], previewLabels: data.previewLabels || [],
previewLimit: data.previewLimit || 3, previewLimit: data.previewLimit || 3,
@@ -173,7 +173,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<div className="grid gap-4"> <div className="grid gap-4">
{isTraefikMeDomain && ( {isTraefikMeDomain && (
<AlertBlock type="info"> <AlertBlock type="info">
<strong>Note:</strong> sslip.io is a public HTTP service and <strong>Note:</strong> traefik.me is a public HTTP service and
does not support SSL/HTTPS. HTTPS and certificate options will does not support SSL/HTTPS. HTTPS and certificate options will
not have any effect. not have any effect.
</AlertBlock> </AlertBlock>
@@ -192,7 +192,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Wildcard Domain</FormLabel> <FormLabel>Wildcard Domain</FormLabel>
<FormControl> <FormControl>
<Input placeholder="*.sslip.io" {...field} /> <Input placeholder="*.traefik.me" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -80,7 +80,6 @@ export const commonCronExpressions = [
const formSchema = z const formSchema = z
.object({ .object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
description: z.string().optional(),
cronExpression: z.string().min(1, "Cron expression is required"), cronExpression: z.string().min(1, "Cron expression is required"),
shellType: z.enum(["bash", "sh"]).default("bash"), shellType: z.enum(["bash", "sh"]).default("bash"),
command: z.string(), command: z.string(),
@@ -225,7 +224,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
resolver: standardSchemaResolver(formSchema), resolver: standardSchemaResolver(formSchema),
defaultValues: { defaultValues: {
name: "", name: "",
description: "",
cronExpression: "", cronExpression: "",
shellType: "bash", shellType: "bash",
command: "", command: "",
@@ -265,7 +263,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
if (scheduleId && schedule) { if (scheduleId && schedule) {
form.reset({ form.reset({
name: schedule.name, name: schedule.name,
description: schedule.description || "",
cronExpression: schedule.cronExpression, cronExpression: schedule.cronExpression,
shellType: schedule.shellType, shellType: schedule.shellType,
command: schedule.command, command: schedule.command,
@@ -482,26 +479,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
)} )}
/> />
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input
placeholder="Backs up the database every day at midnight"
{...field}
/>
</FormControl>
<FormDescription>
Optional description of what this schedule does
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<ScheduleFormField <ScheduleFormField
name="cronExpression" name="cronExpression"
formControl={form.control} formControl={form.control}

View File

@@ -125,11 +125,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
{schedule.enabled ? "Enabled" : "Disabled"} {schedule.enabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
</div> </div>
{schedule.description && (
<p className="text-xs text-muted-foreground/70 [overflow-wrap:anywhere] line-clamp-2">
{schedule.description}
</p>
)}
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap"> <div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
<Badge <Badge
variant="outline" variant="outline"

View File

@@ -2,10 +2,6 @@ import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -40,6 +36,10 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
const DockerLogsId = dynamic( const DockerLogsId = dynamic(
() => () =>

View File

@@ -49,12 +49,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
const composeFile = form.watch("composeFile"); const composeFile = form.watch("composeFile");
useEffect(() => { useEffect(() => {
if (data) { if (data && !composeFile) {
form.reset({ form.reset({
composeFile: data.composeFile || "", composeFile: data.composeFile || "",
}); });
} }
}, [form, data]); }, [form, form.reset, data]);
useEffect(() => { useEffect(() => {
if (data?.composeFile !== undefined) { if (data?.composeFile !== undefined) {

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(), slug: z.string().optional(),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -6,7 +6,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitIcon } from "@/components/icons/data-tools-icons"; import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -42,10 +41,7 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, { repositoryURL: z.string().min(1, {
message: "Repository URL is required", message: "Repository URL is required",
}), }),
branch: z branch: z.string().min(1, "Branch required"),
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -1,11 +1,10 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; import { CheckIcon, ChevronsUpDown, Plus, X, HelpCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -58,10 +57,7 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -1,4 +1,3 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react"; import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"), triggerType: z.enum(["push", "tag"]).default("push"),

View File

@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
gitlabPathNamespace: z.string().min(1), gitlabPathNamespace: z.string().min(1),
}) })
.required(), .required(),
branch: z branch: z.string().min(1, "Branch is required"),
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -288,6 +288,7 @@ export const RestoreBackup = ({
toast.error("Please select a database type"); toast.error("Please select a database type");
return; return;
} }
console.log({ data });
setIsDeploying(true); setIsDeploying(true);
}; };

View File

@@ -1,14 +1,5 @@
"use client"; "use client";
import copy from "copy-to-clipboard"; import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react";
import {
Bot,
Check,
Copy,
Loader2,
RotateCcw,
Settings,
X,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
@@ -39,7 +30,6 @@ const MAX_LOG_LINES = 200;
export function AnalyzeLogs({ logs, context }: Props) { export function AnalyzeLogs({ logs, context }: Props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [aiId, setAiId] = useState<string>(""); const [aiId, setAiId] = useState<string>("");
const [copied, setCopied] = useState(false);
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, { const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
enabled: open, enabled: open,
}); });
@@ -62,15 +52,6 @@ export function AnalyzeLogs({ logs, context }: Props) {
mutate({ aiId, logs: logsText, context }); mutate({ aiId, logs: logsText, context });
}; };
const handleCopy = () => {
if (!data?.analysis) return;
const success = copy(data.analysis);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return ( return (
<Popover <Popover
open={open} open={open}
@@ -90,7 +71,7 @@ export function AnalyzeLogs({ logs, context }: Props) {
disabled={logs.length === 0} disabled={logs.length === 0}
title="Analyze logs with AI" title="Analyze logs with AI"
> >
<Bot className="mr-2 size-4" /> <Bot className="mr-2 h-4 w-4" />
AI AI
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -187,18 +168,6 @@ export function AnalyzeLogs({ logs, context }: Props) {
)} )}
Re-analyze Re-analyze
</Button> </Button>
<Button
size="sm"
variant="outline"
onClick={handleCopy}
title="Copy analysis to clipboard"
>
{copied ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</Button>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"

View File

@@ -347,13 +347,11 @@ export const DockerLogsId: React.FC<Props> = ({
title={isPaused ? "Resume logs" : "Pause logs"} title={isPaused ? "Resume logs" : "Pause logs"}
> >
{isPaused ? ( {isPaused ? (
<Play className="size-4" /> <Play className="mr-2 h-4 w-4" />
) : ( ) : (
<Pause className="size-4" /> <Pause className="mr-2 h-4 w-4" />
)} )}
<span className="hidden lg:ml-2 lg:inline"> {isPaused ? "Resume" : "Pause"}
{isPaused ? "Resume" : "Pause"}
</span>
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -364,13 +362,11 @@ export const DockerLogsId: React.FC<Props> = ({
title="Copy logs to clipboard" title="Copy logs to clipboard"
> >
{copied ? ( {copied ? (
<Check className="size-4" /> <Check className="mr-2 h-4 w-4" />
) : ( ) : (
<Copy className="size-4" /> <Copy className="mr-2 h-4 w-4" />
)} )}
<span className="hidden lg:ml-2 lg:inline"> Copy
{copied ? "Copied" : "Copy"}
</span>
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -378,18 +374,17 @@ export const DockerLogsId: React.FC<Props> = ({
className="h-9 sm:w-auto w-full" className="h-9 sm:w-auto w-full"
onClick={handleDownload} onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name} disabled={filteredLogs.length === 0 || !data?.Name}
title="Download logs as text file"
> >
<DownloadIcon className="size-4" /> <DownloadIcon className="mr-2 h-4 w-4" />
<span className="hidden lg:ml-2 lg:inline">Download logs</span> Download logs
</Button> </Button>
<AnalyzeLogs logs={filteredLogs} context="runtime" /> <AnalyzeLogs logs={filteredLogs} context="runtime" />
</div> </div>
</div> </div>
{isPaused && ( {isPaused && (
<AlertBlock type="warning" className="items-center"> <AlertBlock type="warning">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Pause className="size-4" /> <Pause className="h-4 w-4" />
<span> <span>
Logs paused Logs paused
{messageBuffer.length > 0 && ( {messageBuffer.length > 0 && (

View File

@@ -1,4 +1,3 @@
import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -16,6 +15,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
interface Props { interface Props {

View File

@@ -1,4 +1,3 @@
import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -16,6 +15,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
interface Props { interface Props {

View File

@@ -26,8 +26,8 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
type UploadFileToContainer,
uploadFileToContainerSchema, uploadFileToContainerSchema,
type UploadFileToContainer,
} from "@/utils/schema"; } from "@/utils/schema";
interface Props { interface Props {

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
mariadbId: string; mariadbId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
mongoId: string; mongoId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
mysqlId: string; mysqlId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
postgresId: string; postgresId: string;

View File

@@ -632,6 +632,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
control={form.control} control={form.control}
name="enableNamespaces" name="enableNamespaces"
render={({ field }) => { render={({ field }) => {
console.log(field.value);
return ( return (
<FormItem> <FormItem>
<FormLabel>Enable Namespaces</FormLabel> <FormLabel>Enable Namespaces</FormLabel>

View File

@@ -1,494 +0,0 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { Code2, FileInput, Globe2, HardDrive, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug";
import { api } from "@/utils/api";
import { APP_NAME_MESSAGE, APP_NAME_REGEX } from "@/utils/schema";
const AddImportSchema = z.object({
name: z.string().min(1, { message: "Name is required" }),
appName: z
.string()
.min(1, { message: "App name is required" })
.regex(APP_NAME_REGEX, { message: APP_NAME_MESSAGE }),
base64: z.string().min(1, { message: "Base64 content is required" }),
serverId: z.string().optional(),
});
type AddImport = z.infer<typeof AddImportSchema>;
type TemplateInfo = {
compose: string;
template: {
domains: Array<{
serviceName: string;
port: number;
path?: string;
host?: string;
}>;
envs: string[];
mounts: Array<{ filePath: string; content: string }>;
};
};
interface Props {
environmentId: string;
projectName?: string;
}
export const AddImport = ({ environmentId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [mountOpen, setMountOpen] = useState(false);
const [selectedMount, setSelectedMount] = useState<{
filePath: string;
content: string;
} | null>(null);
const [templateInfo, setTemplateInfo] = useState<TemplateInfo | null>(null);
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const shouldShowServerDropdown = !!(servers && servers.length > 0);
const { mutateAsync: previewTemplate, isPending: isProcessing } =
api.compose.previewTemplate.useMutation();
const { mutateAsync: createCompose, isPending: isCreating } =
api.compose.create.useMutation();
const { mutateAsync: importCompose, isPending: isImporting } =
api.compose.import.useMutation();
const form = useForm<AddImport>({
defaultValues: { name: "", appName: `${slug}-`, base64: "" },
resolver: zodResolver(AddImportSchema),
});
const resetAll = () => {
form.reset({ name: "", appName: `${slug}-`, base64: "" });
setTemplateInfo(null);
setPreviewOpen(false);
setMountOpen(false);
setSelectedMount(null);
};
const handleOpenChange = (open: boolean) => {
if (!open) resetAll();
setVisible(open);
};
const handleLoad = async (data: AddImport) => {
try {
const result = await previewTemplate({
appName: data.appName,
base64: data.base64.trim(),
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
});
setTemplateInfo(result);
setPreviewOpen(true);
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error processing template",
);
}
};
const handleImport = async () => {
const data = form.getValues();
try {
const compose = await createCompose({
name: data.name,
appName: data.appName,
environmentId,
composeType: "docker-compose",
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
});
await importCompose({
composeId: compose.composeId,
base64: data.base64.trim(),
});
toast.success("Compose imported successfully");
await utils.environment.one.invalidate({ environmentId });
resetAll();
setVisible(false);
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Error importing compose",
);
}
};
const handleCancelPreview = () => {
setPreviewOpen(false);
setTemplateInfo(null);
};
return (
<>
<Dialog open={visible} onOpenChange={handleOpenChange}>
<DialogTrigger className="w-full">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<FileInput className="size-4 text-muted-foreground" />
<span>Import</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>Import Compose</DialogTitle>
<DialogDescription>
Paste a base64-encoded compose export to preview and import it
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
id="hook-form-import"
onSubmit={form.handleSubmit(handleLoad)}
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="My App"
{...field}
onChange={(e) => {
const val = e.target.value || "";
form.setValue(
"appName",
`${slug}-${slugify(val.trim())}`,
);
field.onChange(val);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{shouldShowServerDropdown && (
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
If no server is selected, the compose will be
deployed on the server where the user is logged
in.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Select
onValueChange={field.onChange}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue
placeholder={
!isCloud ? "Dokploy" : "Select a Server"
}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
<span className="text-muted-foreground text-xs self-center">
Default
</span>
</span>
</SelectItem>
)}
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
<span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center">
{server.ipAddress}
</span>
</span>
</SelectItem>
))}
<SelectLabel>
Servers (
{(servers?.length ?? 0) + (!isCloud ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="appName"
render={({ field }) => (
<FormItem>
<FormLabel>App Name</FormLabel>
<FormControl>
<Input placeholder="my-app" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="base64"
render={({ field }) => (
<FormItem>
<FormLabel>Configuration (Base64)</FormLabel>
<FormControl>
<Textarea
placeholder="Paste your base64-encoded compose export here..."
className="font-mono resize-none h-32"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
type="submit"
variant="outline"
isLoading={isCreating || isProcessing}
>
Load
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
{/* Preview modal */}
<Dialog
open={previewOpen}
onOpenChange={(open) => !open && handleCancelPreview()}
>
<DialogContent className="max-w-[60vw]">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">
Template Information
</DialogTitle>
<DialogDescription className="space-y-2">
<p>Review the template information before importing</p>
<AlertBlock type="warning">
Warning: This will remove all existing environment variables,
mounts, and domains from this service.
</AlertBlock>
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-6">
<div className="space-y-4">
<div className="flex items-center gap-2">
<Code2 className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Docker Compose</h3>
</div>
<CodeEditor
language="yaml"
value={templateInfo?.compose || ""}
className="font-mono"
readOnly
/>
</div>
{templateInfo?.template.domains &&
templateInfo.template.domains.length > 0 && (
<>
<Separator />
<div className="space-y-4">
<div className="flex items-center gap-2">
<Globe2 className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Domains</h3>
</div>
<div className="grid grid-cols-1 gap-3">
{templateInfo.template.domains.map((domain, index) => (
<div
key={index}
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm"
>
<div className="font-medium">
{domain.serviceName}
</div>
<div className="text-sm text-muted-foreground space-y-1">
<div>Port: {domain.port}</div>
{domain.host && <div>Host: {domain.host}</div>}
{domain.path && <div>Path: {domain.path}</div>}
</div>
</div>
))}
</div>
</div>
</>
)}
{templateInfo?.template.envs &&
templateInfo.template.envs.length > 0 && (
<>
<Separator />
<div className="space-y-4">
<div className="flex items-center gap-2">
<Code2 className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">
Environment Variables
</h3>
</div>
<div className="grid grid-cols-1 gap-2">
{templateInfo.template.envs.map((env, index) => (
<div
key={index}
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
>
{env}
</div>
))}
</div>
</div>
</>
)}
{templateInfo?.template.mounts &&
templateInfo.template.mounts.length > 0 && (
<>
<Separator />
<div className="space-y-4">
<div className="flex items-center gap-2">
<HardDrive className="h-5 w-5 text-primary" />
<h3 className="text-lg font-semibold">Mounts</h3>
</div>
<div className="grid grid-cols-1 gap-2">
{templateInfo.template.mounts.map((mount, index) => (
<div
key={index}
className="rounded-lg border bg-card p-2 font-mono text-sm hover:bg-accent cursor-pointer transition-colors"
onClick={() => {
setSelectedMount(mount);
setMountOpen(true);
}}
>
{mount.filePath}
</div>
))}
</div>
</div>
</>
)}
</div>
<div className="flex justify-end gap-2 pt-4">
<Button variant="outline" onClick={handleCancelPreview}>
Cancel
</Button>
<Button isLoading={isImporting} onClick={handleImport}>
Import
</Button>
</div>
</DialogContent>
</Dialog>
{/* Mount content modal */}
<Dialog open={mountOpen} onOpenChange={setMountOpen}>
<DialogContent className="max-w-[50vw]">
<DialogHeader>
<DialogTitle className="text-xl font-bold">
{selectedMount?.filePath}
</DialogTitle>
<DialogDescription>Mount File Content</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[45vh] pr-4">
<CodeEditor
language="yaml"
value={selectedMount?.content || ""}
className="font-mono"
readOnly
/>
</ScrollArea>
<div className="flex justify-end gap-2 pt-4">
<Button onClick={() => setMountOpen(false)}>Close</Button>
</div>
</DialogContent>
</Dialog>
</>
);
};

View File

@@ -1,6 +1,6 @@
import { import {
Bookmark,
BookText, BookText,
Bookmark,
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
Globe, Globe,

View File

@@ -344,7 +344,7 @@ export const ShowProjects = () => {
} }
}} }}
> >
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border flex flex-col"> <Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between gap-2 overflow-clip"> <CardTitle className="flex items-center justify-between gap-2 overflow-clip">
<span className="flex flex-col gap-1.5 "> <span className="flex flex-col gap-1.5 ">
@@ -491,7 +491,7 @@ export const ShowProjects = () => {
</div> </div>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardFooter className="pt-4 mt-auto"> <CardFooter className="pt-4">
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4"> <div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}> <DateTooltip date={project.createdAt}>
Created Created

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
redisId: string; redisId: string;

View File

@@ -79,11 +79,8 @@ export const columns: ColumnDef<LogEntry>[] = [
: log.RequestPath} : log.RequestPath}
</div> </div>
<div className="flex flex-row gap-3 w-full"> <div className="flex flex-row gap-3 w-full">
<Badge <Badge variant={getStatusColor(log.OriginStatus)}>
variant={getStatusColor(log.OriginStatus || log.DownstreamStatus)} Status: {formatStatusLabel(log.OriginStatus)}
>
Status:{" "}
{formatStatusLabel(log.OriginStatus || log.DownstreamStatus)}
</Badge> </Badge>
<Badge variant={"secondary"}> <Badge variant={"secondary"}>
Exec Time: {formatDuration(log.Duration)} Exec Time: {formatDuration(log.Duration)}

View File

@@ -185,7 +185,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
<div className="flex flex-col gap-4 w-full overflow-auto"> <div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex items-center gap-2 max-sm:flex-wrap"> <div className="flex items-center gap-2 max-sm:flex-wrap">
<Input <Input
placeholder="Filter by hostname..." placeholder="Filter by name..."
value={search} value={search}
onChange={(event) => setSearch(event.target.value)} onChange={(event) => setSearch(event.target.value)}
className="md:max-w-sm" className="md:max-w-sm"

View File

@@ -25,6 +25,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { NumberInput } from "@/components/ui/input";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -33,7 +34,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { NumberInput } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";

View File

@@ -1,5 +1,3 @@
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { import {
@@ -9,6 +7,8 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
interface Props { interface Props {
serverId?: string; serverId?: string;

View File

@@ -3,7 +3,6 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
@@ -27,6 +26,7 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
import { api, type RouterOutputs } from "@/utils/api"; import { api, type RouterOutputs } from "@/utils/api";
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */ /** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */

View File

@@ -141,14 +141,14 @@ export const WebDomain = () => {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 grid-cols-2" className="grid w-full gap-4 md:grid-cols-2"
> >
<FormField <FormField
control={form.control} control={form.control}
name="domain" name="domain"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem className="col-span-2 md:col-span-1"> <FormItem>
<FormLabel>Domain</FormLabel> <FormLabel>Domain</FormLabel>
<FormControl> <FormControl>
<Input <Input
@@ -168,7 +168,7 @@ export const WebDomain = () => {
name="letsEncryptEmail" name="letsEncryptEmail"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem className="col-span-2 md:col-span-1"> <FormItem>
<FormLabel>Let's Encrypt Email</FormLabel> <FormLabel>Let's Encrypt Email</FormLabel>
<FormControl> <FormControl>
<Input <Input
@@ -209,7 +209,7 @@ export const WebDomain = () => {
name="certificateType" name="certificateType"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem className="col-span-2"> <FormItem className="md:col-span-2">
<FormLabel>Certificate Provider</FormLabel> <FormLabel>Certificate Provider</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}

View File

@@ -1,4 +1,4 @@
import { CopyIcon, ServerIcon } from "lucide-react"; import { ServerIcon } from "lucide-react";
import { import {
Card, Card,
CardContent, CardContent,
@@ -7,8 +7,6 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions"; import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions"; import { ShowStorageActions } from "./servers/actions/show-storage-actions";
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions"; import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
@@ -51,17 +49,8 @@ export const WebServer = () => {
</div> </div>
<div className="flex items-center flex-wrap justify-between gap-4"> <div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground flex items-center gap-1.5"> <span className="text-sm text-muted-foreground">
Server IP: {webServerSettings?.serverIp} Server IP: {webServerSettings?.serverIp}
{webServerSettings?.serverIp && (
<CopyIcon
className="size-3.5 cursor-pointer hover:text-foreground transition-colors"
onClick={() => {
copy(webServerSettings.serverIp ?? "");
toast.success("Copied to clipboard");
}}
/>
)}
</span> </span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Version: {dokployVersion} Version: {dokployVersion}

View File

@@ -868,19 +868,6 @@ function SidebarLogo() {
); );
} }
function MobileCloser() {
const pathname = usePathname();
const { setOpenMobile, isMobile } = useSidebar();
useEffect(() => {
if (isMobile) {
setOpenMobile(false);
}
}, [pathname, isMobile, setOpenMobile]);
return null;
}
export default function Page({ children }: Props) { export default function Page({ children }: Props) {
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>( const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
undefined, undefined,
@@ -946,7 +933,6 @@ export default function Page({ children }: Props) {
} as React.CSSProperties } as React.CSSProperties
} }
> >
<MobileCloser />
<Sidebar collapsible="icon" variant="floating"> <Sidebar collapsible="icon" variant="floating">
<SidebarHeader> <SidebarHeader>
{/* <SidebarMenuButton {/* <SidebarMenuButton

View File

@@ -0,0 +1,236 @@
"use client";
import { Copy, KeyRound, Loader2, Plus, Trash2 } from "lucide-react";
import { type ReactNode, useState } from "react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
interface Props {
children: ReactNode;
}
export const ScimDialog = ({ children }: Props) => {
const utils = api.useUtils();
const baseURL = useUrl();
const [open, setOpen] = useState(false);
const [newProviderId, setNewProviderId] = useState("");
const [justCreatedToken, setJustCreatedToken] = useState<{
providerId: string;
token: string;
} | null>(null);
const { data: providers = [], isPending } = api.scim.listProviders.useQuery(
undefined,
{ enabled: open },
);
const { mutateAsync: generateToken, isPending: isGenerating } =
api.scim.generateToken.useMutation();
const { mutateAsync: deleteProvider, isPending: isDeleting } =
api.scim.deleteProvider.useMutation();
const scimUrl = `${baseURL || "{baseURL}"}/api/auth/scim/v2`;
const handleGenerate = async () => {
const providerId = newProviderId.trim().toLowerCase();
if (!providerId) return;
try {
const result = await generateToken({ providerId });
setJustCreatedToken({
providerId: result.providerId,
token: result.scimToken,
});
setNewProviderId("");
await utils.scim.listProviders.invalidate();
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to generate SCIM token",
);
}
};
const handleDelete = async (providerId: string) => {
try {
await deleteProvider({ providerId });
toast.success("SCIM provider removed");
await utils.scim.listProviders.invalidate();
} catch (err) {
toast.error(
err instanceof Error ? err.message : "Failed to delete SCIM provider",
);
}
};
const handleCopy = async (value: string, label: string) => {
try {
await navigator.clipboard.writeText(value);
toast.success(`${label} copied`);
} catch {
toast.error("Failed to copy");
}
};
const handleOpenChange = (next: boolean) => {
setOpen(next);
if (!next) setJustCreatedToken(null);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-[560px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<KeyRound className="size-5" />
SCIM provisioning
</DialogTitle>
<DialogDescription>
Automatically provision, update, and deactivate users from your
identity provider (Okta, Entra ID, etc.). Configure the SCIM endpoint
below in your IdP.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="grid gap-1">
<Label className="text-xs font-medium text-muted-foreground">
SCIM 2.0 endpoint URL
</Label>
<div className="flex items-center gap-2">
<p className="flex-1 break-all rounded-md bg-muted px-2 py-1.5 font-mono text-xs">
{scimUrl}
</p>
<Button
variant="outline"
size="icon"
className="size-8 shrink-0"
onClick={() => handleCopy(scimUrl, "Endpoint URL")}
disabled={!baseURL}
>
<Copy className="size-3.5" />
</Button>
</div>
</div>
{justCreatedToken && (
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3">
<p className="text-sm font-medium">
Bearer token for {justCreatedToken.providerId}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Copy this token now it will not be shown again. Paste it into
your IdP's SCIM configuration.
</p>
<div className="mt-2 flex items-center gap-2">
<p className="flex-1 break-all rounded-md bg-background px-2 py-1.5 font-mono text-xs">
{justCreatedToken.token}
</p>
<Button
variant="outline"
size="icon"
className="size-8 shrink-0"
onClick={() =>
handleCopy(justCreatedToken.token, "Bearer token")
}
>
<Copy className="size-3.5" />
</Button>
</div>
</div>
)}
<div className="space-y-2">
<Label className="text-sm font-medium">
Generate token for a new provider
</Label>
<div className="flex gap-2">
<Input
value={newProviderId}
onChange={(e) => setNewProviderId(e.target.value)}
placeholder="okta, entra, jumpcloud..."
className="font-mono text-sm"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void handleGenerate();
}
}}
/>
<Button
size="sm"
onClick={handleGenerate}
disabled={!newProviderId.trim() || isGenerating}
>
<Plus className="mr-1 size-4" />
Generate
</Button>
</div>
<p className="text-xs text-muted-foreground">
Choose a unique identifier for this IdP connection (lowercase,
alphanumeric, dashes).
</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Existing providers</Label>
{isPending ? (
<div className="flex items-center gap-2 justify-center py-4">
<Loader2 className="size-4 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">Loading...</span>
</div>
) : providers.length === 0 ? (
<p className="rounded-md border border-dashed bg-muted/30 px-3 py-4 text-center text-sm text-muted-foreground">
No SCIM providers configured yet.
</p>
) : (
<ul className="flex flex-col gap-2">
{providers.map((provider) => (
<li
key={provider.id}
className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2"
>
<span className="flex-1 font-mono text-sm">
{provider.providerId}
</span>
<DialogAction
title="Remove SCIM provider"
description={`Remove "${provider.providerId}"? Existing provisioned users will stay but the IdP will no longer be able to sync.`}
type="destructive"
onClick={() => handleDelete(provider.providerId)}
>
<Button
variant="ghost"
size="icon"
className="size-8 shrink-0 text-destructive hover:text-destructive"
disabled={isDeleting}
>
<Trash2 className="size-3.5" />
</Button>
</DialogAction>
</li>
))}
</ul>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -2,6 +2,7 @@
import { import {
Eye, Eye,
KeyRound,
Loader2, Loader2,
LogIn, LogIn,
Pencil, Pencil,
@@ -34,6 +35,7 @@ import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url"; import { useUrl } from "@/utils/hooks/use-url";
import { RegisterOidcDialog } from "./register-oidc-dialog"; import { RegisterOidcDialog } from "./register-oidc-dialog";
import { RegisterSamlDialog } from "./register-saml-dialog"; import { RegisterSamlDialog } from "./register-saml-dialog";
import { ScimDialog } from "./scim-dialog";
type ProviderForDetails = { type ProviderForDetails = {
id: string | null; id: string | null;
@@ -169,15 +171,22 @@ export const SSOSettings = () => {
Users can sign in with their organization&apos;s IdP. Users can sign in with their organization&apos;s IdP.
</CardDescription> </CardDescription>
</div> </div>
<Button <div className="flex flex-wrap gap-2 shrink-0">
variant="outline" <Button
size="sm" variant="outline"
onClick={() => setManageOriginsOpen(true)} size="sm"
className="shrink-0" onClick={() => setManageOriginsOpen(true)}
> >
<Shield className="mr-2 size-4" /> <Shield className="mr-2 size-4" />
Manage origins Manage origins
</Button> </Button>
<ScimDialog>
<Button variant="outline" size="sm">
<KeyRound className="mr-2 size-4" />
Manage SCIM
</Button>
</ScimDialog>
</div>
</div> </div>
{isPending ? ( {isPending ? (

View File

@@ -342,7 +342,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2" className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
> >
<FolderInput className="size-4 text-muted-foreground" /> <FolderInput className="size-4 text-muted-foreground" />
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate"> <span className="font-medium max-w-[150px] truncate">
{currentProject?.name || "Select Project"} {currentProject?.name || "Select Project"}
</span> </span>
<ChevronDown className="size-4 text-muted-foreground" /> <ChevronDown className="size-4 text-muted-foreground" />
@@ -478,7 +478,7 @@ export const AdvanceBreadcrumb = () => {
aria-expanded={environmentOpen} aria-expanded={environmentOpen}
className="h-auto px-2 py-1.5 hover:bg-accent gap-2" className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
> >
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate"> <span className="font-medium max-w-[150px] truncate">
{currentEnvironment?.name || "production"} {currentEnvironment?.name || "production"}
</span> </span>
<ChevronDown className="size-4 text-muted-foreground" /> <ChevronDown className="size-4 text-muted-foreground" />
@@ -533,7 +533,7 @@ export const AdvanceBreadcrumb = () => {
)} )}
{projectEnvironments && projectEnvironments.length === 1 && ( {projectEnvironments && projectEnvironments.length === 1 && (
<p className="text-sm font-normal ml-1 max-w-[50px] md:max-w-[150px] truncate"> <p className="text-sm font-normal ml-1">
{currentEnvironment?.name || "production"} {currentEnvironment?.name || "production"}
</p> </p>
)} )}
@@ -551,7 +551,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2" className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
> >
{getServiceIcon(currentService.type)} {getServiceIcon(currentService.type)}
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate"> <span className="font-medium max-w-[150px] truncate">
{currentService.name} {currentService.name}
</span> </span>
<ChevronDown className="size-4 text-muted-foreground" /> <ChevronDown className="size-4 text-muted-foreground" />
@@ -617,7 +617,7 @@ export const AdvanceBreadcrumb = () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-7 ml-1 hidden md:flex" className="size-7 ml-1"
onClick={() => { onClick={() => {
router.push( router.push(
`/dashboard/project/${projectId}/environment/${environmentId}`, `/dashboard/project/${projectId}/environment/${environmentId}`,

View File

@@ -167,13 +167,7 @@ export const CodeEditor = ({
? css() ? css()
: language === "shell" : language === "shell"
? StreamLanguage.define(shell) ? StreamLanguage.define(shell)
: StreamLanguage.define({ : StreamLanguage.define(properties),
...properties,
// The legacy properties mode lacks comment metadata, so
// CodeMirror's toggle-comment shortcut (Mod-/) has no comment
// token to use. Declare `#` as the line comment for env editors.
languageData: { commentTokens: { line: "#" } },
}),
props.lineWrapping ? EditorView.lineWrapping : [], props.lineWrapping ? EditorView.lineWrapping : [],
language === "yaml" language === "yaml"
? autocompletion({ ? autocompletion({

View File

@@ -63,7 +63,6 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
className={cn( className={cn(
buttonVariants({ variant, size, className }), buttonVariants({ variant, size, className }),
"flex gap-2", "flex gap-2",
className,
)} )}
ref={ref} ref={ref}
{...props} {...props}

View File

@@ -1 +0,0 @@
ALTER TABLE "schedule" ADD COLUMN "description" text;

View File

@@ -0,0 +1,11 @@
CREATE TABLE "scim_provider" (
"id" text PRIMARY KEY NOT NULL,
"provider_id" text NOT NULL,
"scim_token" text NOT NULL,
"organization_id" text,
CONSTRAINT "scim_provider_provider_id_unique" UNIQUE("provider_id"),
CONSTRAINT "scim_provider_scim_token_unique" UNIQUE("scim_token")
);
--> statement-breakpoint
ALTER TABLE "two_factor" ADD COLUMN "verified" boolean DEFAULT true NOT NULL;--> statement-breakpoint
ALTER TABLE "scim_provider" ADD CONSTRAINT "scim_provider_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -1,5 +1,5 @@
{ {
"id": "887c0c81-4af9-477a-ab29-b3ad16f08451", "id": "0e5a80ea-2d66-4bd2-a41d-b92c419e0116",
"prevId": "ef484e78-f78d-4c3f-ae4b-aa123dc77e61", "prevId": "ef484e78-f78d-4c3f-ae4b-aa123dc77e61",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
@@ -789,6 +789,13 @@
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
},
"verified": {
"name": "verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
} }
}, },
"indexes": {}, "indexes": {},
@@ -6714,12 +6721,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"cronExpression": { "cronExpression": {
"name": "cronExpression", "name": "cronExpression",
"type": "text", "type": "text",
@@ -6871,6 +6872,72 @@
"checkConstraints": {}, "checkConstraints": {},
"isRLSEnabled": false "isRLSEnabled": false
}, },
"public.scim_provider": {
"name": "scim_provider",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"scim_token": {
"name": "scim_token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"organization_id": {
"name": "organization_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"scim_provider_organization_id_organization_id_fk": {
"name": "scim_provider_organization_id_organization_id_fk",
"tableFrom": "scim_provider",
"tableTo": "organization",
"columnsFrom": [
"organization_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"scim_provider_provider_id_unique": {
"name": "scim_provider_provider_id_unique",
"nullsNotDistinct": false,
"columns": [
"provider_id"
]
},
"scim_provider_scim_token_unique": {
"name": "scim_provider_scim_token_unique",
"nullsNotDistinct": false,
"columns": [
"scim_token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.security": { "public.security": {
"name": "security", "name": "security",
"schema": "", "schema": "",

View File

@@ -1167,8 +1167,8 @@
{ {
"idx": 166, "idx": 166,
"version": "7", "version": "7",
"when": 1778303519111, "when": 1776576422440,
"tag": "0166_nosy_slapstick", "tag": "0166_overjoyed_big_bertha",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -28,7 +28,6 @@ try {
"wait-for-postgres": "wait-for-postgres.ts", "wait-for-postgres": "wait-for-postgres.ts",
"reset-password": "reset-password.ts", "reset-password": "reset-password.ts",
"reset-2fa": "reset-2fa.ts", "reset-2fa": "reset-2fa.ts",
"migrate-auth-secret": "scripts/migrate-auth-secret.ts",
}, },
bundle: true, bundle: true,
platform: "node", platform: "node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.29.4", "version": "v0.29.0",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -14,7 +14,6 @@
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts", "wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
"reset-password": "node -r dotenv/config dist/reset-password.mjs", "reset-password": "node -r dotenv/config dist/reset-password.mjs",
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs", "reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
"migrate-auth-secret": "node -r dotenv/config dist/migrate-auth-secret.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", "dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts", "migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
@@ -47,8 +46,8 @@
"@ai-sdk/mistral": "^3.0.20", "@ai-sdk/mistral": "^3.0.20",
"@ai-sdk/openai": "^3.0.29", "@ai-sdk/openai": "^3.0.29",
"@ai-sdk/openai-compatible": "^2.0.30", "@ai-sdk/openai-compatible": "^2.0.30",
"@better-auth/api-key": "1.5.4", "@better-auth/api-key": "1.6.5",
"@better-auth/sso": "1.5.4", "@better-auth/sso": "1.6.5",
"@codemirror/autocomplete": "^6.18.6", "@codemirror/autocomplete": "^6.18.6",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
@@ -102,7 +101,7 @@
"ai": "^6.0.86", "ai": "^6.0.86",
"ai-sdk-ollama": "^3.7.0", "ai-sdk-ollama": "^3.7.0",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"better-auth": "1.5.4", "better-auth": "1.6.5",
"bl": "6.0.11", "bl": "6.0.11",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"bullmq": "5.67.3", "bullmq": "5.67.3",
@@ -114,7 +113,7 @@
"dockerode": "4.0.2", "dockerode": "4.0.2",
"dompurify": "^3.3.3", "dompurify": "^3.3.3",
"dotenv": "16.4.5", "dotenv": "16.4.5",
"drizzle-orm": "0.45.1", "drizzle-orm": "0.45.2",
"drizzle-zod": "0.8.3", "drizzle-zod": "0.8.3",
"fancy-ansi": "^0.1.3", "fancy-ansi": "^0.1.3",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
@@ -127,7 +126,7 @@
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nextjs-toploader": "^3.9.17", "nextjs-toploader": "^3.9.17",
"node-os-utils": "2.0.1", "node-os-utils": "2.0.1",
"node-pty": "1.1.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",
@@ -148,7 +147,7 @@
"shell-quote": "^1.8.1", "shell-quote": "^1.8.1",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"ssh2": "~1.16.0", "ssh2": "1.15.0",
"stripe": "17.2.0", "stripe": "17.2.0",
"superjson": "^2.2.2", "superjson": "^2.2.2",
"swagger-ui-react": "^5.31.2", "swagger-ui-react": "^5.31.2",

View File

@@ -12,15 +12,6 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
/**
* Log a webhook handler error server-side without leaking its shape to the HTTP
* response. Drizzle errors carry the raw SQL query, column list and parameters,
* so we never forward the error object to the client.
*/
export const logWebhookError = (context: string, error: unknown) => {
console.error(context, error);
};
/** /**
* Helper function to get package_version from registry_package events * Helper function to get package_version from registry_package events
*/ */
@@ -271,15 +262,14 @@ export default async function handler(
); );
} }
} catch (error) { } catch (error) {
logWebhookError("Error deploying Application:", error); res.status(400).json({ message: "Error deploying Application", error });
res.status(400).json({ message: "Error deploying Application" });
return; return;
} }
res.status(200).json({ message: "Application deployed successfully" }); res.status(200).json({ message: "Application deployed successfully" });
} catch (error) { } catch (error) {
logWebhookError("Error deploying Application:", error); console.log(error);
res.status(400).json({ message: "Error deploying Application" }); res.status(400).json({ message: "Error deploying Application", error });
} }
} }

View File

@@ -12,7 +12,6 @@ import {
extractCommittedPaths, extractCommittedPaths,
extractHash, extractHash,
getProviderByHeader, getProviderByHeader,
logWebhookError,
} from "../[refreshToken]"; } from "../[refreshToken]";
export default async function handler( export default async function handler(
@@ -196,14 +195,13 @@ export default async function handler(
); );
} }
} catch (error) { } catch (error) {
logWebhookError("Error deploying Compose:", error); res.status(400).json({ message: "Error deploying Compose", error });
res.status(400).json({ message: "Error deploying Compose" });
return; return;
} }
res.status(200).json({ message: "Compose deployed successfully" }); res.status(200).json({ message: "Compose deployed successfully" });
} catch (error) { } catch (error) {
logWebhookError("Error deploying Compose:", error); console.log(error);
res.status(400).json({ message: "Error deploying Compose" }); res.status(400).json({ message: "Error deploying Compose", error });
} }
} }

View File

@@ -17,22 +17,13 @@ import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types"; import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup"; import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
import { import { extractCommitMessage, extractHash } from "./[refreshToken]";
extractCommitMessage,
extractHash,
logWebhookError,
} from "./[refreshToken]";
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
) { ) {
const signature = req.headers["x-hub-signature-256"]; const signature = req.headers["x-hub-signature-256"];
if (!signature) {
res.status(401).json({ message: "Missing signature header" });
return;
}
const githubBody = req.body; const githubBody = req.body;
if (!githubBody?.installation?.id) { if (!githubBody?.installation?.id) {
@@ -206,8 +197,10 @@ export default async function handler(
}); });
return; return;
} catch (error) { } catch (error) {
logWebhookError("Error deploying applications on tag:", error); console.error("Error deploying applications on tag:", error);
res.status(400).json({ message: "Error deploying applications on tag" }); res
.status(400)
.json({ message: "Error deploying applications on tag", error });
return; return;
} }
} }
@@ -329,8 +322,7 @@ export default async function handler(
} }
res.status(200).json({ message: `Deployed ${totalApps} apps` }); res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) { } catch (error) {
logWebhookError("Error deploying Application:", error); res.status(400).json({ message: "Error deploying Application", error });
res.status(400).json({ message: "Error deploying Application" });
} }
} else if (req.headers["x-github-event"] === "pull_request") { } else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id; const prId = githubBody?.pull_request?.id;

View File

@@ -84,7 +84,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -95,7 +95,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -104,7 +104,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -32,7 +32,6 @@ import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant"
import { AddApplication } from "@/components/dashboard/project/add-application"; import { AddApplication } from "@/components/dashboard/project/add-application";
import { AddCompose } from "@/components/dashboard/project/add-compose"; import { AddCompose } from "@/components/dashboard/project/add-compose";
import { AddDatabase } from "@/components/dashboard/project/add-database"; import { AddDatabase } from "@/components/dashboard/project/add-database";
import { AddImport } from "@/components/dashboard/project/add-import";
import { AddTemplate } from "@/components/dashboard/project/add-template"; import { AddTemplate } from "@/components/dashboard/project/add-template";
import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector"; import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project"; import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
@@ -1092,10 +1091,6 @@ const EnvironmentPage = (
projectName={projectData?.name} projectName={projectData?.name}
environmentId={environmentId} environmentId={environmentId}
/> />
<AddImport
projectName={projectData?.name}
environmentId={environmentId}
/>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
@@ -1104,7 +1099,7 @@ const EnvironmentPage = (
</div> </div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]"> <CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
<> <>
<div className="flex flex-col gap-4 2xl:flex-row 2xl:items-center 2xl:justify-between"> <div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
@@ -1625,9 +1620,9 @@ const EnvironmentPage = (
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<Link <Link
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`} href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
className="block h-full" className="block"
> >
<Card className="flex flex-col h-full group relative cursor-pointer bg-transparent transition-colors hover:bg-border"> <Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
{service.serverId && ( {service.serverId && (
<div className="absolute -left-1 -top-2"> <div className="absolute -left-1 -top-2">
<ServerIcon className="size-4 text-muted-foreground" /> <ServerIcon className="size-4 text-muted-foreground" />
@@ -1832,7 +1827,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -93,7 +93,6 @@ const Service = (
); );
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery();
@@ -148,9 +147,8 @@ const Service = (
<Badge <Badge
className="cursor-pointer" className="cursor-pointer"
onClick={() => { onClick={() => {
const ip = data?.server?.ipAddress || serverIp; if (data?.server?.ipAddress) {
if (ip) { copy(data.server.ipAddress);
copy(ip);
toast.success("IP Address Copied!"); toast.success("IP Address Copied!");
} }
}} }}
@@ -453,7 +451,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -85,7 +85,6 @@ const Service = (
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({ const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "", projectId: data?.environment?.projectId || "",
}); });
@@ -135,9 +134,8 @@ const Service = (
<Badge <Badge
className="cursor-pointer" className="cursor-pointer"
onClick={() => { onClick={() => {
const ip = data?.server?.ipAddress || serverIp; if (data?.server?.ipAddress) {
if (ip) { copy(data.server.ipAddress);
copy(ip);
toast.success("IP Address Copied!"); toast.success("IP Address Copied!");
} }
}} }}
@@ -457,7 +455,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -1,4 +1,3 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react"; import { HelpCircle, ServerOff } from "lucide-react";
@@ -11,7 +10,6 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactElement, useState } from "react"; import { type ReactElement, useState } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
@@ -63,7 +61,6 @@ const Libsql = (
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
return ( return (
<div className="pb-10"> <div className="pb-10">
@@ -102,14 +99,6 @@ const Libsql = (
<div className="flex flex-col h-fit w-fit gap-2"> <div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2"> <div className="flex flex-row h-fit w-fit gap-2">
<Badge <Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={ variant={
!data?.serverId !data?.serverId
? "default" ? "default"
@@ -318,7 +307,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -1,4 +1,3 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react"; import { HelpCircle, ServerOff } from "lucide-react";
@@ -11,7 +10,6 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactElement, useState } from "react"; import { type ReactElement, useState } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
@@ -65,7 +63,6 @@ const Mariadb = (
const { data: permissions } = api.user.getPermissions.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({ const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "", projectId: data?.environment?.projectId || "",
@@ -114,14 +111,6 @@ const Mariadb = (
<div className="flex flex-col h-fit w-fit gap-2"> <div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2"> <div className="flex flex-row h-fit w-fit gap-2">
<Badge <Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={ variant={
!data?.serverId !data?.serverId
? "default" ? "default"
@@ -347,7 +336,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -1,4 +1,3 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react"; import { HelpCircle, ServerOff } from "lucide-react";
@@ -11,7 +10,6 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactElement, useState } from "react"; import { type ReactElement, useState } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
@@ -65,7 +63,6 @@ const Mongo = (
const { data: permissions } = api.user.getPermissions.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({ const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "", projectId: data?.environment?.projectId || "",
}); });
@@ -113,14 +110,6 @@ const Mongo = (
<div className="flex flex-col h-fit w-fit gap-2"> <div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2"> <div className="flex flex-row h-fit w-fit gap-2">
<Badge <Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={ variant={
!data?.serverId !data?.serverId
? "default" ? "default"
@@ -351,7 +340,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -1,6 +1,5 @@
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react"; import { HelpCircle, ServerOff } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
@@ -11,7 +10,6 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactElement, useState } from "react"; import { type ReactElement, useState } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
@@ -64,7 +62,6 @@ const MySql = (
const { data: permissions } = api.user.getPermissions.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({ const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "", projectId: data?.environment?.projectId || "",
}); });
@@ -113,14 +110,6 @@ const MySql = (
<div className="flex flex-col h-fit w-fit gap-2"> <div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2"> <div className="flex flex-row h-fit w-fit gap-2">
<Badge <Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={ variant={
!data?.serverId !data?.serverId
? "default" ? "default"
@@ -329,7 +318,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -1,4 +1,3 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react"; import { HelpCircle, ServerOff } from "lucide-react";
@@ -11,7 +10,6 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactElement, useState } from "react"; import { type ReactElement, useState } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
@@ -64,7 +62,6 @@ const Postgresql = (
const { data: permissions } = api.user.getPermissions.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({ const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "", projectId: data?.environment?.projectId || "",
}); });
@@ -112,14 +109,6 @@ const Postgresql = (
<div className="flex flex-col h-fit w-fit gap-2"> <div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2"> <div className="flex flex-row h-fit w-fit gap-2">
<Badge <Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={ variant={
!data?.serverId !data?.serverId
? "default" ? "default"
@@ -335,7 +324,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -1,4 +1,3 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react"; import { HelpCircle, ServerOff } from "lucide-react";
@@ -11,7 +10,6 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactElement, useState } from "react"; import { type ReactElement, useState } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { toast } from "sonner";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment"; import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show"; import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
@@ -64,7 +62,6 @@ const Redis = (
const { data: permissions } = api.user.getPermissions.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: serverIp } = api.settings.getIp.useQuery();
const { data: environments } = api.environment.byProjectId.useQuery({ const { data: environments } = api.environment.byProjectId.useQuery({
projectId: data?.environment?.projectId || "", projectId: data?.environment?.projectId || "",
}); });
@@ -112,14 +109,6 @@ const Redis = (
<div className="flex flex-col h-fit w-fit gap-2"> <div className="flex flex-col h-fit w-fit gap-2">
<div className="flex flex-row h-fit w-fit gap-2"> <div className="flex flex-row h-fit w-fit gap-2">
<Badge <Badge
className="cursor-pointer"
onClick={() => {
const ip = data?.server?.ipAddress || serverIp;
if (ip) {
copy(ip);
toast.success("IP Address Copied!");
}
}}
variant={ variant={
!data?.serverId !data?.serverId
? "default" ? "default"
@@ -340,7 +329,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -56,7 +56,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -17,7 +17,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -26,7 +26,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -34,7 +34,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -43,7 +43,7 @@ export async function getServerSideProps(
if (!user || (user.role !== "owner" && user.role !== "admin")) { if (!user || (user.role !== "owner" && user.role !== "admin")) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { destination: "/", permanent: false }, redirect: { destination: "/", permanent: true },
}; };
} }

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (!IS_CLOUD) { if (!IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -33,7 +33,7 @@ export async function getServerSideProps(
if (!user || user.role !== "owner") { if (!user || user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -36,7 +36,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -53,7 +53,7 @@ export async function getServerSideProps(
if (!userPermissions?.gitProviders.read) { if (!userPermissions?.gitProviders.read) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (!IS_CLOUD) { if (!IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -33,7 +33,7 @@ export async function getServerSideProps(
if (!user || user.role !== "owner") { if (!user || user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -38,7 +38,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -46,7 +46,7 @@ export async function getServerSideProps(
if (user.role !== "owner") { if (user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -44,7 +44,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -53,7 +53,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -61,7 +61,7 @@ export async function getServerSideProps(
if (user.role === "member") { if (user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -36,7 +36,7 @@ export async function getServerSideProps(
if (user.role === "member") { if (user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!userPermissions?.sshKeys.read) { if (!userPermissions?.sshKeys.read) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (user.role === "member") { if (user.role === "member") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -47,7 +47,7 @@ export async function getServerSideProps(
if (!userPermissions?.tag.read) { if (!userPermissions?.tag.read) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -39,7 +39,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -66,7 +66,7 @@ export async function getServerSideProps(
if (!userPermissions?.member.read) { if (!userPermissions?.member.read) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/", destination: "/",
}, },
}; };
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (user.role !== "owner") { if (user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: false, permanent: true,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

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