Compare commits

..

1 Commits

Author SHA1 Message Date
Mauricio Siu
9bf4a97cee refactor: update color system to use oklch color format
- Changed color definitions in tailwind.config.ts and various components to use oklch format for improved color manipulation.
- Updated button backgrounds in multiple components to enhance visibility and consistency across light and dark themes.
- Adjusted chart color configurations to align with the new color system.
- Refined global CSS variables for better color management in light and dark modes.
2026-04-13 21:34:26 -06:00
197 changed files with 835 additions and 29822 deletions

View File

@@ -138,8 +138,6 @@ jobs:
needs: [combine-manifests]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -162,80 +160,3 @@ jobs:
prerelease: false
env:
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

@@ -68,66 +68,3 @@ jobs:
echo "✅ OpenAPI synced to website successfully"
- name: Sync to MCP repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
cd mcp-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 MCP repository successfully"
- name: Sync to CLI repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
cd cli-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 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"

View File

@@ -4,8 +4,5 @@
"editor.codeActionsOnSave": {
"source.fixAll.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
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 ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]

8403
api-1.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,3 @@
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
PORT=3000
NODE_ENV=development
# Managed Servers (Dokploy Cloud only) — API token from https://hpanel.hostinger.com/profile/api
HOSTINGER_API_KEY=

View File

@@ -494,49 +494,4 @@ describe("processTemplate", () => {
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);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect(
domain.endsWith(`${mockSchema.serverIp.replaceAll(".", "-")}.sslip.io`),
domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy();
});
});

View File

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

View File

@@ -225,7 +225,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const https = form.watch("https");
const domainType = form.watch("domainType");
const host = form.watch("host");
const isTraefikMeDomain = host?.includes("sslip.io") || false;
const isTraefikMeDomain = host?.includes("traefik.me") || false;
useEffect(() => {
if (data) {
@@ -513,7 +513,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
render={({ field }) => (
<FormItem>
{!canGenerateTraefikMeDomains &&
field.value.includes("sslip.io") && (
field.value.includes("traefik.me") && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
@@ -524,12 +524,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to make your sslip.io domain work.
to make your traefik.me domain work.
</AlertBlock>
)}
{isTraefikMeDomain && (
<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
certificate options will not have any effect.
</AlertBlock>
@@ -567,7 +567,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate sslip.io domain</p>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -666,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<div className="space-y-0.5">
<FormLabel>Custom Entrypoint</FormLabel>
<FormDescription>
Use custom entrypoint for domain
Use custom entrypoint for domina
<br />
"web" and/or "websecure" is used by default.
</FormDescription>

View File

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

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),
@@ -245,7 +241,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -333,7 +329,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

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

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -73,10 +72,7 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),
@@ -258,7 +254,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -353,7 +349,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"),
@@ -233,7 +229,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -320,7 +316,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
id: z.number().nullable(),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
@@ -254,7 +250,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -351,7 +347,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -58,7 +58,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</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}>
{canDeploy && (
<DialogAction
@@ -274,14 +274,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
>
<Button
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" />
Open Terminal
</Button>
</DockerTerminalModal>
{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>
<Switch
aria-label="Toggle autodeploy"
@@ -305,7 +305,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
)}
{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>
<Switch
aria-label="Toggle clean cache"

View File

@@ -87,7 +87,7 @@ export const AddPreviewDomain = ({
});
const host = form.watch("host");
const isTraefikMeDomain = host?.includes("sslip.io") || false;
const isTraefikMeDomain = host?.includes("traefik.me") || false;
useEffect(() => {
if (data) {
@@ -162,7 +162,7 @@ export const AddPreviewDomain = ({
<FormItem>
{isTraefikMeDomain && (
<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
certificate options will not have any effect.
</AlertBlock>
@@ -202,7 +202,7 @@ export const AddPreviewDomain = ({
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate sslip.io domain</p>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

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

View File

@@ -80,7 +80,6 @@ export const commonCronExpressions = [
const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
cronExpression: z.string().min(1, "Cron expression is required"),
shellType: z.enum(["bash", "sh"]).default("bash"),
command: z.string(),
@@ -225,7 +224,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
resolver: standardSchemaResolver(formSchema),
defaultValues: {
name: "",
description: "",
cronExpression: "",
shellType: "bash",
command: "",
@@ -265,7 +263,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
if (scheduleId && schedule) {
form.reset({
name: schedule.name,
description: schedule.description || "",
cronExpression: schedule.cronExpression,
shellType: schedule.shellType,
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
name="cronExpression"
formControl={form.control}
@@ -534,7 +511,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -125,11 +125,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
{schedule.enabled ? "Enabled" : "Disabled"}
</Badge>
</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">
<Badge
variant="outline"

View File

@@ -181,7 +181,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -263,7 +263,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -2,10 +2,6 @@ import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic";
import { useState } from "react";
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 { Button } from "@/components/ui/button";
import {
@@ -221,24 +217,6 @@ const ContainerRow = ({
View Logs
</DropdownMenuItem>
</DialogTrigger>
<ShowContainerConfig
containerId={container.containerId}
serverId={serverId || ""}
/>
<ShowContainerMounts
containerId={container.containerId}
serverId={serverId || ""}
/>
<ShowContainerNetworks
containerId={container.containerId}
serverId={serverId || ""}
/>
<DockerTerminalModal
containerId={container.containerId}
serverId={serverId || ""}
>
Terminal
</DockerTerminalModal>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"

View File

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

View File

@@ -5,7 +5,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
@@ -247,7 +243,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -335,7 +331,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

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

View File

@@ -1,11 +1,10 @@
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 { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -58,10 +57,7 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
@@ -244,7 +240,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -331,7 +327,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

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 { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link";
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"),
@@ -234,7 +230,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -321,7 +317,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
gitlabPathNamespace: z.string().min(1),
})
.required(),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
@@ -256,7 +252,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -353,7 +349,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -409,7 +409,7 @@ export const HandleBackup = ({
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -288,6 +288,7 @@ export const RestoreBackup = ({
toast.error("Please select a database type");
return;
}
console.log({ data });
setIsDeploying(true);
};
@@ -345,7 +346,7 @@ export const RestoreBackup = ({
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -427,7 +428,7 @@ export const RestoreBackup = ({
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

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

View File

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

View File

@@ -1,112 +0,0 @@
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
interface Mount {
Type: string;
Source: string;
Destination: string;
Mode: string;
RW: boolean;
Propagation: string;
Name?: string;
Driver?: string;
}
export const ShowContainerMounts = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,
},
);
const mounts: Mount[] = data?.Mounts ?? [];
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
View Mounts
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
<DialogHeader>
<DialogTitle>Container Mounts</DialogTitle>
<DialogDescription>
Volume and bind mounts for this container
</DialogDescription>
</DialogHeader>
<div className="overflow-auto max-h-[70vh]">
{mounts.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No mounts found for this container.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Source</TableHead>
<TableHead>Destination</TableHead>
<TableHead>Mode</TableHead>
<TableHead>Read/Write</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mounts.map((mount, index) => (
<TableRow key={index}>
<TableCell>
<Badge variant="outline">{mount.Type}</Badge>
</TableCell>
<TableCell className="font-mono text-xs max-w-[250px] truncate">
{mount.Name || mount.Source}
</TableCell>
<TableCell className="font-mono text-xs max-w-[250px] truncate">
{mount.Destination}
</TableCell>
<TableCell className="text-xs">
{mount.Mode || "-"}
</TableCell>
<TableCell>
<Badge variant={mount.RW ? "default" : "secondary"}>
{mount.RW ? "RW" : "RO"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,119 +0,0 @@
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
interface Network {
IPAMConfig: unknown;
Links: unknown;
Aliases: string[] | null;
MacAddress: string;
NetworkID: string;
EndpointID: string;
Gateway: string;
IPAddress: string;
IPPrefixLen: number;
IPv6Gateway: string;
GlobalIPv6Address: string;
GlobalIPv6PrefixLen: number;
DriverOpts: unknown;
}
export const ShowContainerNetworks = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,
},
);
const networks: Record<string, Network> =
data?.NetworkSettings?.Networks ?? {};
const entries = Object.entries(networks);
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
View Networks
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
<DialogHeader>
<DialogTitle>Container Networks</DialogTitle>
<DialogDescription>
Networks attached to this container
</DialogDescription>
</DialogHeader>
<div className="overflow-auto max-h-[70vh]">
{entries.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No networks found for this container.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Network</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Gateway</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>Aliases</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{entries.map(([name, network]) => (
<TableRow key={name}>
<TableCell>
<Badge variant="outline">{name}</Badge>
</TableCell>
<TableCell className="font-mono text-xs">
{network.IPAddress
? `${network.IPAddress}/${network.IPPrefixLen}`
: "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{network.Gateway || "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{network.MacAddress || "-"}
</TableCell>
<TableCell className="text-xs">
{network.Aliases?.join(", ") || "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -10,8 +10,6 @@ import {
} from "@/components/ui/dropdown-menu";
import { ShowContainerConfig } from "../config/show-container-config";
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
import { ShowContainerMounts } from "../mounts/show-container-mounts";
import { ShowContainerNetworks } from "../networks/show-container-networks";
import { RemoveContainerDialog } from "../remove/remove-container";
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
import { UploadFileModal } from "../upload/upload-file-modal";
@@ -125,14 +123,6 @@ export const columns: ColumnDef<Container>[] = [
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<ShowContainerMounts
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<ShowContainerNetworks
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<DockerTerminalModal
containerId={container.containerId}
serverId={container.serverId || ""}

View File

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

View File

@@ -1,291 +0,0 @@
import { formatDistanceToNow } from "date-fns";
import { ArrowRight, Rocket, Server } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
type DeploymentStatus = "idle" | "running" | "done" | "error";
const statusDotClass: Record<string, string> = {
done: "bg-emerald-500",
running: "bg-amber-500",
error: "bg-red-500",
idle: "bg-muted-foreground/40",
};
function getServiceInfo(d: any) {
const app = d.application;
const comp = d.compose;
const serverName: string =
d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy";
if (app?.environment?.project && app.environment) {
return {
name: app.name as string,
environment: app.environment.name as string,
projectName: app.environment.project.name as string,
serverName,
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
};
}
if (comp?.environment?.project && comp.environment) {
return {
name: comp.name as string,
environment: comp.environment.name as string,
projectName: comp.environment.project.name as string,
serverName,
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
};
}
return null;
}
function StatCard({
label,
value,
delta,
}: {
label: string;
value: string;
delta?: string;
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<div className="flex flex-col gap-1">
<span className="text-3xl font-semibold tracking-tight">{value}</span>
{delta && (
<span className="text-xs text-muted-foreground">{delta}</span>
)}
</div>
</div>
);
}
function StatusListCard({
label,
items,
}: {
label: string;
items: { dotClass: string; label: string; count: number }[];
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<ul className="flex flex-col gap-1.5">
{items.map((item) => (
<li key={item.label} className="flex items-center gap-2.5 text-sm">
<span
className={`size-2 rounded-full shrink-0 ${item.dotClass}`}
aria-hidden
/>
<span className="font-semibold tabular-nums w-8">{item.count}</span>
<span className="text-muted-foreground">{item.label}</span>
</li>
))}
</ul>
</div>
);
}
export const ShowHome = () => {
const { data: auth } = api.user.get.useQuery();
const { data: homeStats } = api.project.homeStats.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const canReadDeployments = !!permissions?.deployment.read;
const { data: deployments } = api.deployment.allCentralized.useQuery(
undefined,
{
enabled: canReadDeployments,
refetchInterval: 10000,
},
);
const firstName = auth?.user?.firstName?.trim();
const totals = homeStats ?? {
projects: 0,
environments: 0,
applications: 0,
compose: 0,
databases: 0,
services: 0,
};
const statusBreakdown = homeStats?.status ?? {
running: 0,
error: 0,
idle: 0,
};
const recentDeployments = useMemo(() => {
if (!deployments) return [];
return [...deployments]
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
.slice(0, 10);
}, [deployments]);
const deployStats = useMemo(() => {
const now = Date.now();
const weekMs = 7 * 24 * 60 * 60 * 1000;
const lastStart = now - weekMs;
const prevStart = now - 2 * weekMs;
const last: NonNullable<typeof deployments> = [];
const prev: NonNullable<typeof deployments> = [];
for (const d of deployments ?? []) {
const t = new Date(d.createdAt).getTime();
if (t >= lastStart) last.push(d);
else if (t >= prevStart) prev.push(d);
}
const lastCount = last.length;
const prevCount = prev.length;
let delta: string | undefined;
if (prevCount > 0) {
const pct = Math.round(((lastCount - prevCount) / prevCount) * 100);
delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`;
} else if (lastCount > 0) {
delta = "no prior data";
} else {
delta = "no activity yet";
}
return { value: String(lastCount), delta };
}, [deployments]);
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full">
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<h1 className="text-3xl font-semibold tracking-tight">
{firstName ? `Welcome back, ${firstName}` : "Welcome back"}
</h1>
<Button asChild variant="secondary" className="w-fit">
<Link href="/dashboard/projects">
Go to projects
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Projects"
value={String(totals.projects)}
delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`}
/>
<StatCard
label="Services"
value={String(totals.services)}
delta={`${totals.applications} apps · ${totals.compose} compose · ${totals.databases} db`}
/>
<StatCard
label="Deploys / 7d"
value={deployStats.value}
delta={deployStats.delta}
/>
<StatusListCard
label="Status"
items={[
{
dotClass: "bg-emerald-500",
label: "running",
count: statusBreakdown.running,
},
{
dotClass: "bg-red-500",
label: "errored",
count: statusBreakdown.error,
},
{
dotClass: "bg-muted-foreground/40",
label: "idle",
count: statusBreakdown.idle,
},
]}
/>
</div>
<div className="rounded-xl border bg-background">
<div className="flex items-center justify-between px-5 py-4 border-b">
<div className="flex items-center gap-2">
<Rocket className="size-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Recent deployments</h2>
</div>
{canReadDeployments && (
<Link
href="/dashboard/deployments"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
view all
</Link>
)}
</div>
{!canReadDeployments ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>You do not have permission to view deployments.</span>
</div>
) : recentDeployments.length === 0 ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>No deployments yet.</span>
</div>
) : (
<ul className="divide-y">
{recentDeployments.map((d) => {
const info = getServiceInfo(d);
if (!info) return null;
const status = (d.status ?? "idle") as DeploymentStatus;
return (
<li key={d.deploymentId}>
<Link
href={info.href}
className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors"
>
<span
className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`}
aria-hidden
/>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-sm truncate">{info.name}</span>
<span className="text-xs text-muted-foreground truncate">
{info.projectName} · {info.environment}
</span>
</div>
<span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate">
<Server className="size-3 shrink-0" />
<span className="truncate">{info.serverName}</span>
</span>
<span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline">
{status}
</span>
<span className="text-xs text-muted-foreground w-24 text-right hidden md:inline">
{formatDistanceToNow(new Date(d.createdAt), {
addSuffix: true,
})}
</span>
<span className="text-xs text-muted-foreground hover:text-foreground transition-colors">
logs
</span>
</Link>
</li>
);
})}
</ul>
)}
</div>
</div>
</Card>
</div>
);
};

View File

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

View File

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

View File

@@ -17,11 +17,11 @@ interface Props {
const chartConfig = {
readMb: {
label: "Read (MB)",
color: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
writeMb: {
label: "Write (MB)",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;

View File

@@ -17,7 +17,7 @@ interface Props {
const chartConfig = {
usage: {
label: "CPU Usage",
color: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
} satisfies ChartConfig;

View File

@@ -16,7 +16,7 @@ interface Props {
const chartConfig = {
usedGb: {
label: "Used (GB)",
color: "hsl(var(--chart-3))",
color: "oklch(var(--chart-3))",
},
} satisfies ChartConfig;

View File

@@ -25,19 +25,19 @@ const chartConfig = {
},
images: {
label: "Images",
color: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
containers: {
label: "Containers",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
volumes: {
label: "Volumes",
color: "hsl(var(--chart-3))",
color: "oklch(var(--chart-3))",
},
buildCache: {
label: "Build Cache",
color: "hsl(var(--chart-4))",
color: "oklch(var(--chart-4))",
},
} satisfies ChartConfig;
@@ -138,7 +138,7 @@ export const DockerDiskUsageChart = () => {
innerRadius={60}
outerRadius={85}
strokeWidth={3}
stroke="hsl(var(--background))"
stroke="oklch(var(--background))"
minAngle={15}
>
{chartData.map((entry) => (

View File

@@ -19,7 +19,7 @@ interface Props {
const chartConfig = {
usage: {
label: "Memory (GB)",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;

View File

@@ -17,11 +17,11 @@ interface Props {
const chartConfig = {
inMB: {
label: "In (MB)",
color: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
outMB: {
label: "Out (MB)",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;

View File

@@ -27,7 +27,7 @@ interface Props {
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
} satisfies ChartConfig;
@@ -58,12 +58,12 @@ export const ContainerCPUChart = ({ data }: Props) => {
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -112,7 +112,7 @@ export const ContainerCPUChart = ({ data }: Props) => {
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
stroke="oklch(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -33,7 +33,7 @@ interface Props {
const chartConfig = {
memory: {
label: "Memory",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;
@@ -73,12 +73,12 @@ export const ContainerMemoryChart = ({ data }: Props) => {
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -133,7 +133,7 @@ export const ContainerMemoryChart = ({ data }: Props) => {
dataKey="memory"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
stroke="oklch(var(--chart-2))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -40,11 +40,11 @@ interface FormattedMetric {
const chartConfig = {
input: {
label: "Input",
color: "hsl(var(--chart-3))",
color: "oklch(var(--chart-3))",
},
output: {
label: "Output",
color: "hsl(var(--chart-4))",
color: "oklch(var(--chart-4))",
},
} satisfies ChartConfig;
@@ -84,24 +84,24 @@ export const ContainerNetworkChart = ({ data }: Props) => {
<linearGradient id="fillInput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-3))"
stopColor="oklch(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-3))"
stopColor="oklch(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillOutput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-4))"
stopColor="oklch(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-4))"
stopColor="oklch(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -162,7 +162,7 @@ export const ContainerNetworkChart = ({ data }: Props) => {
dataKey="input"
type="monotone"
fill="url(#fillInput)"
stroke="hsl(var(--chart-3))"
stroke="oklch(var(--chart-3))"
strokeWidth={2}
/>
<Area
@@ -170,7 +170,7 @@ export const ContainerNetworkChart = ({ data }: Props) => {
dataKey="output"
type="monotone"
fill="url(#fillOutput)"
stroke="hsl(var(--chart-4))"
stroke="oklch(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -22,7 +22,7 @@ interface CPUChartProps {
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
} satisfies ChartConfig;
@@ -45,12 +45,12 @@ export function CPUChart({ data }: CPUChartProps) {
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -99,7 +99,7 @@ export function CPUChart({ data }: CPUChartProps) {
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
stroke="oklch(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -29,14 +29,14 @@ export function DiskChart({ data }: RadialChartProps) {
const chartData = [
{
disk: 25,
fill: "hsl(var(--chart-2))",
fill: "oklch(var(--chart-2))",
},
];
const chartConfig = {
disk: {
label: "Disk",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;
@@ -71,7 +71,7 @@ export function DiskChart({ data }: RadialChartProps) {
dataKey="disk"
background
cornerRadius={10}
fill="hsl(var(--chart-2))"
fill="oklch(var(--chart-2))"
/>
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label

View File

@@ -20,7 +20,7 @@ interface MemoryChartProps {
const chartConfig = {
Memory: {
label: "Memory",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;
@@ -46,12 +46,12 @@ export function MemoryChart({ data }: MemoryChartProps) {
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -116,7 +116,7 @@ export function MemoryChart({ data }: MemoryChartProps) {
dataKey="memUsed"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
stroke="oklch(var(--chart-2))"
strokeWidth={2}
name="Memory"
/>

View File

@@ -22,11 +22,11 @@ interface NetworkChartProps {
const chartConfig = {
networkIn: {
label: "Network In",
color: "hsl(var(--chart-3))",
color: "oklch(var(--chart-3))",
},
networkOut: {
label: "Network Out",
color: "hsl(var(--chart-4))",
color: "oklch(var(--chart-4))",
},
} satisfies ChartConfig;
@@ -52,24 +52,24 @@ export function NetworkChart({ data }: NetworkChartProps) {
<linearGradient id="fillNetworkIn" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-3))"
stopColor="oklch(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-3))"
stopColor="oklch(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillNetworkOut" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-4))"
stopColor="oklch(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-4))"
stopColor="oklch(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -121,7 +121,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
dataKey="networkIn"
type="monotone"
fill="url(#fillNetworkIn)"
stroke="hsl(var(--chart-3))"
stroke="oklch(var(--chart-3))"
strokeWidth={2}
/>
<Area
@@ -129,7 +129,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
dataKey="networkOut"
type="monotone"
fill="url(#fillNetworkOut)"
stroke="hsl(var(--chart-4))"
stroke="oklch(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend

View File

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

View File

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

View File

@@ -632,6 +632,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
control={form.control}
name="enableNamespaces"
render={({ field }) => {
console.log(field.value);
return (
<FormItem>
<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 {
Bookmark,
BookText,
Bookmark,
CheckIcon,
ChevronsUpDown,
Globe,
@@ -236,7 +236,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
<Button
variant="outline"
className={cn(
"w-full sm:w-[200px] justify-between !bg-input",
"w-full sm:w-[200px] justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
)}
>
{isLoadingTags

View File

@@ -166,7 +166,6 @@ export const ShowProjects = () => {
return (
total +
(env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
@@ -179,7 +178,6 @@ export const ShowProjects = () => {
return (
total +
(env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
@@ -344,7 +342,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>
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
<span className="flex flex-col gap-1.5 ">
@@ -491,7 +489,7 @@ export const ShowProjects = () => {
</div>
</CardTitle>
</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">
<DateTooltip date={project.createdAt}>
Created

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ const chartConfig = {
},
count: {
label: "Count",
color: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
} satisfies ChartConfig;
@@ -101,9 +101,9 @@ export const RequestDistributionChart = ({
<Area
dataKey="count"
type="monotone"
fill="hsl(var(--chart-1))"
fill="oklch(var(--chart-1))"
fillOpacity={0.4}
stroke="hsl(var(--chart-1))"
stroke="oklch(var(--chart-1))"
/>
</AreaChart>
</ChartContainer>

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 items-center gap-2 max-sm:flex-wrap">
<Input
placeholder="Filter by hostname..."
placeholder="Filter by name..."
value={search}
onChange={(event) => setSearch(event.target.value)}
className="md:max-w-sm"

View File

@@ -167,7 +167,7 @@ export const SearchCommand = () => {
<CommandGroup heading={"Application"} hidden={true}>
<CommandItem
onSelect={() => {
router.push("/dashboard/home");
router.push("/dashboard/projects");
setOpen(false);
}}
>

View File

@@ -1,4 +1,4 @@
import { CreditCard, FileText, Server } from "lucide-react";
import { CreditCard, FileText } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import {
@@ -17,11 +17,6 @@ const navigationItems = [
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Managed Servers",
href: "/dashboard/settings/managed-servers",
icon: Server,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",

View File

@@ -9,7 +9,6 @@ import {
Loader2,
MinusIcon,
PlusIcon,
Server,
ShieldCheck,
} from "lucide-react";
import Link from "next/link";
@@ -26,6 +25,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { NumberInput } from "@/components/ui/input";
import {
Dialog,
DialogContent,
@@ -34,7 +34,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { NumberInput } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { Switch } from "@/components/ui/switch";
@@ -83,11 +82,6 @@ const navigationItems = [
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Managed Servers",
href: "/dashboard/settings/managed-servers",
icon: Server,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",

View File

@@ -1,493 +0,0 @@
import {
AlertCircle,
CheckCircle2,
Clock,
CreditCard,
ExternalLink,
FileText,
Loader2,
Plus,
Server,
Trash2,
XCircle,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
const navigationItems = [
{
name: "Subscription",
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Managed Servers",
href: "/dashboard/settings/managed-servers",
icon: Server,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",
icon: FileText,
},
];
const STATUS_MAP: Record<
string,
{
label: string;
icon: React.ReactNode;
variant: "default" | "secondary" | "destructive" | "outline";
}
> = {
pending: {
label: "Pending",
icon: <Clock className="size-3" />,
variant: "secondary",
},
provisioning: {
label: "Provisioning",
icon: <Loader2 className="size-3 animate-spin" />,
variant: "secondary",
},
configuring: {
label: "Installing Dokploy",
icon: <Loader2 className="size-3 animate-spin" />,
variant: "secondary",
},
ready: {
label: "Ready",
icon: <CheckCircle2 className="size-3" />,
variant: "default",
},
error: {
label: "Error",
icon: <XCircle className="size-3" />,
variant: "destructive",
},
terminating: {
label: "Terminating",
icon: <Loader2 className="size-3 animate-spin" />,
variant: "secondary",
},
terminated: {
label: "Terminated",
icon: <AlertCircle className="size-3" />,
variant: "outline",
},
};
function formatSpecs(cpus: number, memoryMb: number, diskMb: number, bandwidthMb: number) {
const bandwidthTb = bandwidthMb / 1024 / 1024;
const bandwidthLabel = bandwidthTb >= 1 ? `${bandwidthTb.toFixed(0)} TB` : `${Math.round(bandwidthMb / 1024)} GB`;
return `${cpus} vCPU · ${Math.round(memoryMb / 1024)} GB RAM · ${Math.round(diskMb / 1024)} GB NVMe · ${bandwidthLabel} bandwidth`;
}
function centsToDisplay(cents: number) {
return (cents / 100).toFixed(2).replace(/\.00$/, "");
}
function OrderServerDialog({ onSuccess }: { onSuccess: () => void }) {
const [open, setOpen] = useState(false);
const [selectedPlan, setSelectedPlan] = useState<string>("");
const [selectedDc, setSelectedDc] = useState<string>("");
const [isAnnual, setIsAnnual] = useState(false);
const { data: plans, isLoading: loadingPlans } =
api.managedServer.getPlans.useQuery(undefined, { enabled: open });
const { data: dataCenters, isLoading: loadingDcs } =
api.managedServer.getDataCenters.useQuery(undefined, { enabled: open });
const isLoadingOptions = loadingPlans || loadingDcs;
const purchase = api.managedServer.purchase.useMutation({
onSuccess: () => {
toast.success("Server order placed! Provisioning will take ~5 minutes.");
setOpen(false);
onSuccess();
},
onError: (err) => {
toast.error(err.message);
},
});
const plan = plans?.find((p) => p.id === selectedPlan);
const displayPrice = (p: NonNullable<typeof plan>) =>
isAnnual
? `$${centsToDisplay(p.dokployPriceCentsAnnual)}/yr`
: `$${centsToDisplay(p.dokployPriceCentsMonthly)}/mo`;
const displayPriceSmall = (p: NonNullable<typeof plan>) =>
isAnnual
? `$${centsToDisplay(Math.round(p.dokployPriceCentsAnnual / 12))}/mo billed annually`
: `$${centsToDisplay(p.dokployPriceCentsAnnual)}/yr if annual`;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="size-4 mr-2" />
Order Server
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Order a Managed Server</DialogTitle>
<DialogDescription>
We'll provision and configure a server for you automatically. Ready
in ~5 minutes.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 pt-2">
{isLoadingOptions ? (
<div className="flex flex-col items-center justify-center py-8 gap-3 text-muted-foreground">
<Loader2 className="size-6 animate-spin" />
<p className="text-sm">Loading available plans...</p>
</div>
) : (
<div className="space-y-4">
{/* Billing period toggle */}
<div className="flex items-center gap-1 rounded-lg border p-1 bg-muted/40 w-fit">
<button
type="button"
onClick={() => setIsAnnual(false)}
className={cn(
"px-4 py-1.5 rounded-md text-sm font-medium transition-colors",
!isAnnual
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
Monthly
</button>
<button
type="button"
onClick={() => setIsAnnual(true)}
className={cn(
"px-4 py-1.5 rounded-md text-sm font-medium transition-colors flex items-center gap-1.5",
isAnnual
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
Annual
<span className="text-xs bg-green-500/15 text-green-600 dark:text-green-400 px-1.5 py-0.5 rounded font-semibold">
Save ~20%
</span>
</button>
</div>
{/* Plan selector */}
<div className="space-y-2">
<Label>Plan</Label>
<div className="grid gap-2">
{plans?.map((p) => (
<button
key={p.id}
type="button"
onClick={() => setSelectedPlan(p.id)}
className={cn(
"flex items-center justify-between rounded-lg border p-3 text-left transition-colors",
selectedPlan === p.id
? "border-primary bg-primary/5"
: "border-border hover:border-muted-foreground",
)}
>
<div>
<p className="font-medium text-sm">{p.name}</p>
<p className="text-xs text-muted-foreground">
{formatSpecs(p.cpus, p.memoryMb, p.diskMb, p.bandwidthMb)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-sm">
{displayPrice(p)}
</p>
<p className="text-xs text-muted-foreground">
{displayPriceSmall(p)}
</p>
</div>
</button>
))}
</div>
</div>
{/* Data center selector */}
<div className="space-y-2">
<Label>Data Center</Label>
<Select value={selectedDc} onValueChange={setSelectedDc}>
<SelectTrigger>
<SelectValue placeholder="Select a location..." />
</SelectTrigger>
<SelectContent position="popper" side="bottom" sideOffset={4} className="max-h-56 overflow-y-auto">
{dataCenters?.map((dc) => (
<SelectItem key={dc.id} value={String(dc.id)}>
{dc.city} — {dc.continent}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{plan && selectedDc && (
<div className="rounded-lg bg-muted p-3 text-sm space-y-1">
<div className="flex justify-between">
<span className="text-muted-foreground">Plan</span>
<span className="font-medium">{plan.name}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Billing</span>
<span className="font-medium">{isAnnual ? "Annual" : "Monthly"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Total</span>
<span className="font-semibold">{displayPrice(plan)}</span>
</div>
</div>
)}
<Button
className="w-full"
disabled={!selectedPlan || !selectedDc || purchase.isPending}
onClick={() => {
if (!selectedPlan || !selectedDc) return;
purchase.mutate({
plan: selectedPlan,
dataCenterId: Number(selectedDc),
isAnnual,
});
}}
>
{purchase.isPending ? (
<>
<Loader2 className="size-4 mr-2 animate-spin" />
Placing order...
</>
) : (
"Order Server"
)}
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}
export const ShowManagedServers = () => {
const router = useRouter();
const utils = api.useUtils();
const { data: servers, isLoading } = api.managedServer.list.useQuery();
const syncStatus = api.managedServer.syncStatus.useMutation({
onSuccess: () => utils.managedServer.list.invalidate(),
});
const deleteServer = api.managedServer.delete.useMutation({
onSuccess: () => {
toast.success("Server terminated.");
utils.managedServer.list.invalidate();
},
onError: (err) => toast.error(err.message),
});
return (
<div className="w-full">
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<CardHeader>
<CardTitle className="text-xl flex flex-row gap-2">
<Server className="size-6 text-muted-foreground self-center" />
Billing
</CardTitle>
<CardDescription>
Manage your subscription and servers
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 py-4 border-t">
<nav className="flex space-x-2 border-b">
{navigationItems.map((item) => {
const Icon = item.icon;
const isActive = router.pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
isActive
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
)}
>
<Icon className="h-4 w-4" />
{item.name}
</Link>
);
})}
</nav>
<div className="mt-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-base">Managed Servers</h3>
<p className="text-sm text-muted-foreground">
Servers provisioned and managed by Dokploy Cloud
</p>
</div>
<OrderServerDialog
onSuccess={() => utils.managedServer.list.invalidate()}
/>
</div>
{isLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
) : servers?.length === 0 ? (
<div className="text-center py-12 border rounded-lg border-dashed">
<Server className="size-10 mx-auto text-muted-foreground mb-3" />
<p className="text-sm font-medium">No managed servers yet</p>
<p className="text-xs text-muted-foreground mt-1">
Order a server and we'll provision and configure it for you
automatically.
</p>
</div>
) : (
<div className="space-y-3">
{servers?.map((s) => {
const status =
STATUS_MAP[s.status] ?? STATUS_MAP.error!;
const isProvisioning = [
"pending",
"provisioning",
"configuring",
].includes(s.status);
const planLabel = s.plan
.split("-")
.slice(-2)
.join(" ")
.toUpperCase();
return (
<div
key={s.managedServerId}
className="flex items-center justify-between rounded-lg border p-4"
>
<div className="flex items-center gap-3">
<Server className="size-5 text-muted-foreground shrink-0" />
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">
{planLabel}
</span>
<Badge
variant={status?.variant}
className="flex items-center gap-1 text-xs h-5"
>
{status?.icon}
{status?.label}
</Badge>
</div>
<p className="text-xs text-muted-foreground">
{s.hostname ?? ""}
{s.ipAddress ? ` · ${s.ipAddress}` : ""}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{isProvisioning && (
<Button
variant="ghost"
size="sm"
onClick={() =>
syncStatus.mutate({
managedServerId: s.managedServerId,
})
}
disabled={syncStatus.isPending}
>
<Loader2
className={cn(
"size-4",
syncStatus.isPending && "animate-spin",
)}
/>
</Button>
)}
{s.status === "ready" && s.server && (
<Button variant="outline" size="sm" asChild>
<Link
href={`/dashboard/settings/server?serverId=${s.serverId}`}
>
<ExternalLink className="size-3.5 mr-1.5" />
Open
</Link>
</Button>
)}
<DialogAction
title="Terminate Server"
description="This will permanently destroy the server and all data on it. This action cannot be undone."
type="destructive"
onClick={() =>
deleteServer.mutate({
managedServerId: s.managedServerId,
})
}
>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
>
<Trash2 className="size-4" />
</Button>
</DialogAction>
</div>
</div>
);
})}
</div>
)}
</div>
</CardContent>
</div>
</Card>
</div>
);
};

View File

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

View File

@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
onClick={() => {
if (stepper.isLast) {
setIsOpen(false);
push("/dashboard/home");
push("/dashboard/projects");
} else {
stepper.next();
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { CopyIcon, ServerIcon } from "lucide-react";
import { ServerIcon } from "lucide-react";
import {
Card,
CardContent,
@@ -7,8 +7,6 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
@@ -51,17 +49,8 @@ export const WebServer = () => {
</div>
<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}
{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 className="text-sm text-muted-foreground">
Version: {dokployVersion}

View File

@@ -19,7 +19,6 @@ import {
Forward,
GalleryVerticalEnd,
GitBranch,
House,
Key,
KeyRound,
Loader2,
@@ -149,12 +148,6 @@ type Menu = {
// The `isEnabled` function is called to determine if the item should be displayed
const MENU: Menu = {
home: [
{
isSingle: true,
title: "Home",
url: "/dashboard/home",
icon: House,
},
{
isSingle: true,
title: "Projects",
@@ -868,19 +861,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) {
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
undefined,
@@ -946,7 +926,6 @@ export default function Page({ children }: Props) {
} as React.CSSProperties
}
>
<MobileCloser />
<Sidebar collapsible="icon" variant="floating">
<SidebarHeader>
{/* <SidebarMenuButton

View File

@@ -80,7 +80,7 @@ export const UserNav = () => {
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/home");
router.push("/dashboard/projects");
}}
>
Projects

View File

@@ -44,7 +44,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
try {
const { data, error } = await authClient.signIn.sso({
email: values.email,
callbackURL: "/dashboard/home",
callbackURL: "/dashboard/projects",
});
if (error) {
toast.error(error.message ?? "Failed to sign in with SSO");

View File

@@ -60,99 +60,99 @@ const DEFAULT_CSS_TEMPLATE = `/* ============================================
/* ---------- Light Mode ---------- */
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--background: 1 0 0;
--foreground: 0.145 0 0;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--card: 1 0 0;
--card-foreground: 0.145 0 0;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--popover: 1 0 0;
--popover-foreground: 0.145 0 0;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--primary: 0.205 0 0;
--primary-foreground: 0.985 0 0;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--secondary: 0.97 0 0;
--secondary-foreground: 0.205 0 0;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--muted: 0.97 0 0;
--muted-foreground: 0.556 0 0;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--accent: 0.97 0 0;
--accent-foreground: 0.205 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--destructive: 0.577 0.245 27.325;
--destructive-foreground: 0.985 0 0;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
--border: 0.922 0 0;
--input: 0.922 0 0;
--ring: 0.708 0 0;
--radius: 0.625rem;
/* Sidebar */
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar: 0.985 0 0;
--sidebar-foreground: 0.145 0 0;
--sidebar-primary: 0.205 0 0;
--sidebar-primary-foreground: 0.985 0 0;
--sidebar-accent: 0.97 0 0;
--sidebar-accent-foreground: 0.205 0 0;
--sidebar-border: 0.922 0 0;
--sidebar-ring: 0.708 0 0;
/* Charts */
--chart-1: 173 58% 39%;
--chart-2: 12 76% 61%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--chart-1: 0.646 0.222 41.116;
--chart-2: 0.6 0.118 184.704;
--chart-3: 0.398 0.07 227.392;
--chart-4: 0.828 0.189 84.429;
--chart-5: 0.769 0.188 70.08;
}
/* ---------- Dark Mode ---------- */
.dark {
--background: 0 0% 0%;
--foreground: 0 0% 98%;
--background: 0.145 0 0;
--foreground: 0.985 0 0;
--card: 240 4% 10%;
--card-foreground: 0 0% 98%;
--card: 0.205 0 0;
--card-foreground: 0.985 0 0;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--popover: 0.205 0 0;
--popover-foreground: 0.985 0 0;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--primary: 0.922 0 0;
--primary-foreground: 0.205 0 0;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--secondary: 0.269 0 0;
--secondary-foreground: 0.985 0 0;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--muted: 0.269 0 0;
--muted-foreground: 0.708 0 0;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--accent: 0.269 0 0;
--accent-foreground: 0.985 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--destructive: 0.704 0.191 22.216;
--destructive-foreground: 0.985 0 0;
--border: 240 3.7% 15.9%;
--input: 240 4% 10%;
--ring: 240 4.9% 83.9%;
--border: 0.371 0 0;
--input: 0.371 0 0;
--ring: 0.556 0 0;
/* Sidebar */
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar: 0.205 0 0;
--sidebar-foreground: 0.985 0 0;
--sidebar-primary: 0.488 0.243 264.376;
--sidebar-primary-foreground: 0.985 0 0;
--sidebar-accent: 0.269 0 0;
--sidebar-accent-foreground: 0.985 0 0;
--sidebar-border: 0.371 0 0;
--sidebar-ring: 0.556 0 0;
/* Charts */
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-1: 0.488 0.243 264.376;
--chart-2: 0.696 0.17 162.48;
--chart-3: 0.769 0.188 70.08;
--chart-4: 0.627 0.265 303.9;
--chart-5: 0.645 0.246 16.439;
}
/* ---------- Custom Styles ---------- */

View File

@@ -342,7 +342,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
<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"}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
@@ -478,7 +478,7 @@ export const AdvanceBreadcrumb = () => {
aria-expanded={environmentOpen}
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"}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
@@ -533,7 +533,7 @@ export const AdvanceBreadcrumb = () => {
)}
{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"}
</p>
)}
@@ -551,7 +551,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
{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}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
@@ -617,7 +617,7 @@ export const AdvanceBreadcrumb = () => {
<Button
variant="ghost"
size="icon"
className="size-7 ml-1 hidden md:flex"
className="size-7 ml-1"
onClick={() => {
router.push(
`/dashboard/project/${projectId}/environment/${environmentId}`,

View File

@@ -116,14 +116,6 @@ export function TagSelector({
<HandleTag />
</div>
</CommandEmpty>
{tags.length === 0 && (
<div className="flex flex-col items-center gap-2 py-4">
<span className="text-sm text-muted-foreground">
No tags created yet.
</span>
<HandleTag />
</div>
)}
<CommandGroup>
{tags.map((tag) => {
const isSelected = selectedTags.includes(tag.id);

View File

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

View File

@@ -13,7 +13,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground p-px",
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
@@ -39,12 +39,12 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<div className="flex h-9 items-center gap-2 border-b px-3" cmdk-input-wrapper="">
<Search className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -115,7 +115,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}

View File

@@ -76,8 +76,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={inputType}
className={cn(
// bg-gray
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 md:text-sm",
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
className,
)}

View File

@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-input px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30 [&>span]:line-clamp-1",
className,
)}
{...props}

View File

@@ -528,7 +528,7 @@ const sidebarMenuButtonVariants = cva(
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
"bg-background shadow-[0_0_0_1px_oklch(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_oklch(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",

View File

@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/80",
"peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
className,
)}
{...props}
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
"pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
)}
/>
</SwitchPrimitives.Root>

View File

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

View File

@@ -1,22 +0,0 @@
CREATE TYPE "public"."managedServerStatus" AS ENUM('pending', 'provisioning', 'configuring', 'ready', 'error', 'terminating', 'terminated');--> statement-breakpoint
CREATE TABLE "managed_server" (
"managedServerId" text PRIMARY KEY NOT NULL,
"organizationId" text NOT NULL,
"serverId" text,
"plan" text NOT NULL,
"status" "managedServerStatus" DEFAULT 'pending' NOT NULL,
"hostingerVmId" integer,
"hostingerSubscriptionId" text,
"dataCenterId" integer NOT NULL,
"ipAddress" text,
"hostname" text,
"stripeSubscriptionId" text,
"stripePriceId" text,
"rootPassword" text,
"errorMessage" text,
"createdAt" text NOT NULL,
"updatedAt" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "managed_server" ADD CONSTRAINT "managed_server_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "managed_server" ADD CONSTRAINT "managed_server_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE set null ON UPDATE no action;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1163,20 +1163,6 @@
"when": 1775845419261,
"tag": "0165_abnormal_greymalkin",
"breakpoints": true
},
{
"idx": 166,
"version": "7",
"when": 1778303519111,
"tag": "0166_nosy_slapstick",
"breakpoints": true
},
{
"idx": 167,
"version": "7",
"when": 1778657133470,
"tag": "0167_dizzy_solo",
"breakpoints": true
}
]
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.29.4",
"version": "v0.29.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -14,7 +14,6 @@
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
"reset-password": "node -r dotenv/config dist/reset-password.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 ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
@@ -127,7 +126,7 @@
"next-themes": "^0.2.1",
"nextjs-toploader": "^3.9.17",
"node-os-utils": "2.0.1",
"node-pty": "1.1.0",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",
@@ -148,7 +147,7 @@
"shell-quote": "^1.8.1",
"slugify": "^1.6.6",
"sonner": "^1.7.4",
"ssh2": "~1.16.0",
"ssh2": "1.15.0",
"stripe": "17.2.0",
"superjson": "^2.2.2",
"swagger-ui-react": "^5.31.2",

View File

@@ -48,7 +48,7 @@ const MyApp = ({
disableTransitionOnChange
forcedTheme={Component.theme}
>
<NextTopLoader color="hsl(var(--sidebar-ring))" />
<NextTopLoader color="oklch(var(--sidebar-ring))" />
<WhitelabelingProvider />
<Toaster richColors />
<SearchCommand />

View File

@@ -53,7 +53,7 @@ export default function Custom404({ statusCode, error }: Props) {
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
<Link
href="/dashboard/home"
href="/dashboard/projects"
className={buttonVariants({
variant: "secondary",
className: "flex flex-row gap-2",

View File

@@ -12,15 +12,6 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
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
*/
@@ -271,15 +262,14 @@ export default async function handler(
);
}
} catch (error) {
logWebhookError("Error deploying Application:", error);
res.status(400).json({ message: "Error deploying Application" });
res.status(400).json({ message: "Error deploying Application", error });
return;
}
res.status(200).json({ message: "Application deployed successfully" });
} catch (error) {
logWebhookError("Error deploying Application:", error);
res.status(400).json({ message: "Error deploying Application" });
console.log(error);
res.status(400).json({ message: "Error deploying Application", error });
}
}

View File

@@ -12,7 +12,6 @@ import {
extractCommittedPaths,
extractHash,
getProviderByHeader,
logWebhookError,
} from "../[refreshToken]";
export default async function handler(
@@ -196,14 +195,13 @@ export default async function handler(
);
}
} catch (error) {
logWebhookError("Error deploying Compose:", error);
res.status(400).json({ message: "Error deploying Compose" });
res.status(400).json({ message: "Error deploying Compose", error });
return;
}
res.status(200).json({ message: "Compose deployed successfully" });
} catch (error) {
logWebhookError("Error deploying Compose:", error);
res.status(400).json({ message: "Error deploying Compose" });
console.log(error);
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 { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import {
extractCommitMessage,
extractHash,
logWebhookError,
} from "./[refreshToken]";
import { extractCommitMessage, extractHash } from "./[refreshToken]";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const signature = req.headers["x-hub-signature-256"];
if (!signature) {
res.status(401).json({ message: "Missing signature header" });
return;
}
const githubBody = req.body;
if (!githubBody?.installation?.id) {
@@ -206,8 +197,10 @@ export default async function handler(
});
return;
} catch (error) {
logWebhookError("Error deploying applications on tag:", error);
res.status(400).json({ message: "Error deploying applications on tag" });
console.error("Error deploying applications on tag:", error);
res
.status(400)
.json({ message: "Error deploying applications on tag", error });
return;
}
}
@@ -329,8 +322,7 @@ export default async function handler(
}
res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) {
logWebhookError("Error deploying Application:", error);
res.status(400).json({ message: "Error deploying Application" });
res.status(400).json({ message: "Error deploying Application", error });
}
} else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id;

View File

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

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/home",
destination: "/dashboard/projects",
},
};
}

View File

@@ -1,53 +0,0 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowHome } from "@/components/dashboard/home/show-home";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
const Home = () => {
return <ShowHome />;
};
export default Home;
Home.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
permanent: false,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
await helpers.user.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

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

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