mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-27 16:55:25 +02:00
Merge branch 'canary' into auto-password-generator
This commit is contained in:
@@ -13,7 +13,6 @@
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.14.3",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"@nerimity/mimiqueue": "1.2.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"hono": "^4.7.10",
|
||||
"pino": "9.4.0",
|
||||
|
||||
@@ -25,7 +25,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
titleLog: z.string().optional(),
|
||||
descriptionLog: z.string().optional(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy"]),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application-preview"),
|
||||
serverId: z.string().min(1),
|
||||
}),
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
deployPreviewApplication,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildPreviewApplication,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updatePreviewDeployment,
|
||||
@@ -54,7 +55,14 @@ export const deploy = async (job: DeployJob) => {
|
||||
previewStatus: "running",
|
||||
});
|
||||
if (job.server) {
|
||||
if (job.type === "deploy") {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildPreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Rebuild Preview Deployment",
|
||||
descriptionLog: job.descriptionLog || "",
|
||||
previewDeploymentId: job.previewDeploymentId,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployPreviewApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog || "Preview Deployment",
|
||||
|
||||
@@ -25,7 +25,7 @@ if (typeof window === "undefined") {
|
||||
}
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
railpackVersion: "0.15.4",
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
createEnvFile: true,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createRouterConfig } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
railpackVersion: "0.15.4",
|
||||
rollbackActive: false,
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
|
||||
@@ -65,7 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.railpack),
|
||||
railpackVersion: z.string().nullable().default("0.2.2"),
|
||||
railpackVersion: z.string().nullable().default("0.15.4"),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal(BuildType.static),
|
||||
@@ -186,7 +186,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
||||
railpackVersion:
|
||||
data.buildType === BuildType.railpack
|
||||
? data.railpackVersion || "0.2.2"
|
||||
? data.railpackVersion || "0.15.4"
|
||||
: null,
|
||||
})
|
||||
.then(async () => {
|
||||
|
||||
@@ -232,9 +232,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
{field.value.gitlabPathNamespace && (
|
||||
<Link
|
||||
href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`}
|
||||
href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
GitPullRequest,
|
||||
Hammer,
|
||||
Loader2,
|
||||
PenSquare,
|
||||
RocketIcon,
|
||||
@@ -22,6 +23,13 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
@@ -38,6 +46,9 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||
api.previewDeployment.delete.useMutation();
|
||||
|
||||
const { mutateAsync: redeployPreviewDeployment } =
|
||||
api.previewDeployment.redeploy.useMutation();
|
||||
|
||||
const {
|
||||
data: previewDeployments,
|
||||
refetch: refetchPreviewDeployments,
|
||||
@@ -46,6 +57,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
{ applicationId },
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
refetchInterval: (data) =>
|
||||
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -193,6 +206,58 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
</Button>
|
||||
</ShowDeploymentsModal>
|
||||
|
||||
<DialogAction
|
||||
title="Rebuild Preview Deployment"
|
||||
description="Are you sure you want to rebuild this preview deployment?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeployPreviewDeployment({
|
||||
previewDeploymentId:
|
||||
deployment.previewDeploymentId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Preview deployment rebuild started",
|
||||
);
|
||||
refetchPreviewDeployments();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error rebuilding preview deployment",
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
isLoading={status === "running"}
|
||||
className="gap-2"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2">
|
||||
<Hammer className="size-4" />
|
||||
Rebuild
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="z-[60]"
|
||||
>
|
||||
<p>
|
||||
Rebuild the preview deployment without
|
||||
downloading new code
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
|
||||
<AddPreviewDomain
|
||||
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||
domainId={deployment.domain?.domainId}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -97,6 +97,16 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
const repository = form.watch("repository");
|
||||
const gitlabId = form.watch("gitlabId");
|
||||
|
||||
const gitlabUrl = useMemo(() => {
|
||||
const url = gitlabProviders?.find(
|
||||
(provider) => provider.gitlabId === gitlabId,
|
||||
)?.gitlabUrl;
|
||||
|
||||
const gitlabUrl = url?.replace(/\/$/, "");
|
||||
|
||||
return gitlabUrl || "https://gitlab.com";
|
||||
}, [gitlabId, gitlabProviders]);
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
@@ -224,9 +234,9 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<FormItem className="md:col-span-2 flex flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Repository</FormLabel>
|
||||
{field.value.owner && field.value.repo && (
|
||||
{field.value.gitlabPathNamespace && (
|
||||
<Link
|
||||
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
|
||||
href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CreditCard, FileText } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ShowInvoices } from "./show-invoices";
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Subscription",
|
||||
href: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/settings/invoices",
|
||||
icon: FileText,
|
||||
},
|
||||
];
|
||||
|
||||
export const ShowBillingInvoices = () => {
|
||||
const router = useRouter();
|
||||
|
||||
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">
|
||||
<CreditCard className="size-6 text-muted-foreground self-center" />
|
||||
Billing
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your subscription and invoices
|
||||
</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">
|
||||
<ShowInvoices />
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,11 +4,13 @@ import {
|
||||
AlertTriangle,
|
||||
CheckIcon,
|
||||
CreditCard,
|
||||
FileText,
|
||||
Loader2,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -37,7 +39,22 @@ export const calculatePrice = (count: number, isAnnual = false) => {
|
||||
if (count <= 1) return 4.5;
|
||||
return count * 3.5;
|
||||
};
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Subscription",
|
||||
href: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/settings/invoices",
|
||||
icon: FileText,
|
||||
},
|
||||
];
|
||||
|
||||
export const ShowBilling = () => {
|
||||
const router = useRouter();
|
||||
const { data: servers } = api.server.count.useQuery();
|
||||
const { data: admin } = api.user.get.useQuery();
|
||||
const { data, isLoading } = api.stripe.getProducts.useQuery();
|
||||
@@ -76,17 +93,41 @@ export const ShowBilling = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<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">
|
||||
<CreditCard className="size-6 text-muted-foreground self-center" />
|
||||
Billing
|
||||
</CardTitle>
|
||||
<CardDescription>Manage your subscription</CardDescription>
|
||||
<CardDescription>
|
||||
Manage your subscription and invoices
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<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="flex flex-col gap-4 w-full mt-6">
|
||||
<Tabs
|
||||
defaultValue="monthly"
|
||||
value={isAnnual ? "annual" : "monthly"}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Download, ExternalLink, FileText, Loader2 } from "lucide-react";
|
||||
import type Stripe from "stripe";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const formatDate = (timestamp: number | null) => {
|
||||
if (!timestamp) return "-";
|
||||
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: currency.toUpperCase(),
|
||||
}).format(amount / 100);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: Stripe.Invoice.Status | null) => {
|
||||
const statusConfig: Record<
|
||||
Stripe.Invoice.Status,
|
||||
{ label: string; variant: "default" | "secondary" | "destructive" }
|
||||
> = {
|
||||
paid: { label: "Paid", variant: "default" },
|
||||
open: { label: "Open", variant: "secondary" },
|
||||
draft: { label: "Draft", variant: "secondary" },
|
||||
void: { label: "Void", variant: "destructive" },
|
||||
uncollectible: { label: "Uncollectible", variant: "destructive" },
|
||||
};
|
||||
|
||||
if (!status) {
|
||||
return <Badge variant="secondary">Unknown</Badge>;
|
||||
}
|
||||
|
||||
const config = statusConfig[status] || {
|
||||
label: status,
|
||||
variant: "secondary" as const,
|
||||
};
|
||||
|
||||
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||||
};
|
||||
|
||||
export const ShowInvoices = () => {
|
||||
const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center min-h-[20vh]">
|
||||
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center">
|
||||
Loading invoices...
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
</div>
|
||||
) : invoices && invoices.length > 0 ? (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Invoice</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Due Date</TableHead>
|
||||
<TableHead>Amount</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{invoices.map((invoice) => (
|
||||
<TableRow key={invoice.id}>
|
||||
<TableCell className="font-medium">
|
||||
{invoice.number || invoice.id.slice(0, 12)}
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(invoice.created)}</TableCell>
|
||||
<TableCell>{formatDate(invoice.dueDate)}</TableCell>
|
||||
<TableCell>
|
||||
{formatAmount(invoice.amountDue, invoice.currency)}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(invoice.status)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{invoice.hostedInvoiceUrl && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
invoice.hostedInvoiceUrl || "",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{invoice.invoicePdf && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
window.open(invoice.invoicePdf || "", "_blank")
|
||||
}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center min-h-[20vh] gap-2">
|
||||
<FileText className="size-12 text-muted-foreground" />
|
||||
<p className="text-base text-muted-foreground">No invoices found</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your invoices will appear here once you have a subscription
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
@@ -89,15 +88,15 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
<Button
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Setup Server
|
||||
</DropdownMenuItem>
|
||||
Setup Server <Settings className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-4xl ">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -6,9 +6,7 @@ import {
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Network,
|
||||
Pencil,
|
||||
ServerIcon,
|
||||
Settings,
|
||||
Terminal,
|
||||
Trash2,
|
||||
User,
|
||||
@@ -31,9 +29,7 @@ import {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
@@ -285,7 +281,32 @@ export const ShowServers = () => {
|
||||
|
||||
{/* Compact Actions */}
|
||||
{isActive && (
|
||||
<div className="flex items-center gap-2 pt-3 border-t mt-auto">
|
||||
<div className="flex items-center gap-2 pt-3 border-t mt-auto flex-wrap">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="max-w-xs"
|
||||
side="bottom"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="font-semibold">
|
||||
Setup Server
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure and initialize your
|
||||
server with Docker, Traefik, and
|
||||
other essential services
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<TooltipProvider>
|
||||
{server.sshKeyId && (
|
||||
<Tooltip>
|
||||
@@ -311,20 +332,6 @@ export const ShowServers = () => {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Setup Server</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
|
||||
1
apps/dokploy/drizzle/0134_strong_hercules.sql
Normal file
1
apps/dokploy/drizzle/0134_strong_hercules.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ALTER COLUMN "railpackVersion" SET DEFAULT '0.15.4';
|
||||
6968
apps/dokploy/drizzle/meta/0134_snapshot.json
Normal file
6968
apps/dokploy/drizzle/meta/0134_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -939,6 +939,13 @@
|
||||
"when": 1766301478005,
|
||||
"tag": "0133_striped_the_order",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 134,
|
||||
"version": "7",
|
||||
"when": 1767871040249,
|
||||
"tag": "0134_strong_hercules",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.26.3",
|
||||
"version": "v0.26.4",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -153,9 +153,11 @@
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"yaml": "2.8.1",
|
||||
"zod": "^3.25.32",
|
||||
"zod-form-data": "^2.0.7"
|
||||
"zod-form-data": "^2.0.7",
|
||||
"semver": "7.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
|
||||
63
apps/dokploy/pages/dashboard/settings/invoices.tsx
Normal file
63
apps/dokploy/pages/dashboard/settings/invoices.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
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 { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Page = () => {
|
||||
return <ShowBillingInvoices />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Page.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout metaName="Invoices">{page}</DashboardLayout>;
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
if (!IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role !== "owner") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
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.user.get.prefetch();
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -285,6 +285,7 @@ export const backupRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
return true;
|
||||
}),
|
||||
listBackupFiles: protectedProcedure
|
||||
|
||||
@@ -2,11 +2,15 @@ import {
|
||||
findApplicationById,
|
||||
findPreviewDeploymentById,
|
||||
findPreviewDeploymentsByApplicationId,
|
||||
IS_CLOUD,
|
||||
removePreviewDeployment,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { apiFindAllByApplication } 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 { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const previewDeploymentRouter = createTRPCRouter({
|
||||
@@ -60,4 +64,55 @@ export const previewDeploymentRouter = createTRPCRouter({
|
||||
}
|
||||
return previewDeployment;
|
||||
}),
|
||||
redeploy: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
previewDeploymentId: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const previewDeployment = await findPreviewDeploymentById(
|
||||
input.previewDeploymentId,
|
||||
);
|
||||
if (
|
||||
previewDeployment.application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this preview deployment",
|
||||
});
|
||||
}
|
||||
const application = await findApplicationById(
|
||||
previewDeployment.applicationId,
|
||||
);
|
||||
const jobData: DeploymentJob = {
|
||||
applicationId: previewDeployment.applicationId,
|
||||
titleLog: input.title || "Rebuild Preview Deployment",
|
||||
descriptionLog: input.description || "",
|
||||
type: "redeploy",
|
||||
applicationType: "application-preview",
|
||||
previewDeploymentId: input.previewDeploymentId,
|
||||
server: !!application.serverId,
|
||||
};
|
||||
|
||||
if (IS_CLOUD && application.serverId) {
|
||||
jobData.serverId = application.serverId;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await reloadDockerResource("dokploy");
|
||||
await reloadDockerResource("dokploy", undefined, packageInfo.version);
|
||||
return true;
|
||||
}),
|
||||
cleanRedis: adminProcedure.mutation(async () => {
|
||||
@@ -399,7 +399,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
return await getUpdateData();
|
||||
return await getUpdateData(packageInfo.version);
|
||||
}),
|
||||
updateServer: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
|
||||
@@ -81,6 +81,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
metadata: {
|
||||
adminId: owner.id,
|
||||
},
|
||||
customer_email: owner.email,
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
|
||||
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||
@@ -128,4 +129,39 @@ export const stripeRouter = createTRPCRouter({
|
||||
|
||||
return servers.length < user.serversQuantity;
|
||||
}),
|
||||
|
||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
if (!stripeCustomerId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
});
|
||||
|
||||
try {
|
||||
const invoices = await stripe.invoices.list({
|
||||
customer: stripeCustomerId,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return invoices.data.map((invoice) => ({
|
||||
id: invoice.id,
|
||||
number: invoice.number,
|
||||
status: invoice.status,
|
||||
amountDue: invoice.amount_due,
|
||||
amountPaid: invoice.amount_paid,
|
||||
currency: invoice.currency,
|
||||
created: invoice.created,
|
||||
dueDate: invoice.due_date,
|
||||
hostedInvoiceUrl: invoice.hosted_invoice_url,
|
||||
invoicePdf: invoice.invoice_pdf,
|
||||
}));
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
deployPreviewApplication,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildPreviewApplication,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
updatePreviewDeployment,
|
||||
@@ -54,7 +55,14 @@ export const deploymentWorker = new Worker(
|
||||
previewStatus: "running",
|
||||
});
|
||||
|
||||
if (job.data.type === "deploy") {
|
||||
if (job.data.type === "redeploy") {
|
||||
await rebuildPreviewApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
descriptionLog: job.data.descriptionLog,
|
||||
previewDeploymentId: job.data.previewDeploymentId,
|
||||
});
|
||||
} else if (job.data.type === "deploy") {
|
||||
await deployPreviewApplication({
|
||||
applicationId: job.data.applicationId,
|
||||
titleLog: job.data.titleLog,
|
||||
|
||||
@@ -22,7 +22,7 @@ type DeployJob =
|
||||
titleLog: string;
|
||||
descriptionLog: string;
|
||||
server?: boolean;
|
||||
type: "deploy";
|
||||
type: "deploy" | "redeploy";
|
||||
applicationType: "application-preview";
|
||||
previewDeploymentId: string;
|
||||
serverId?: string;
|
||||
|
||||
@@ -58,7 +58,7 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta
|
||||
WITH recent_metrics AS (
|
||||
SELECT metrics_json
|
||||
FROM container_metrics
|
||||
WHERE container_name LIKE ? || '%'
|
||||
WHERE container_name = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
)
|
||||
@@ -98,7 +98,7 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e
|
||||
WITH recent_metrics AS (
|
||||
SELECT metrics_json
|
||||
FROM container_metrics
|
||||
WHERE container_name LIKE ? || '%'
|
||||
WHERE container_name = ?
|
||||
ORDER BY timestamp DESC
|
||||
)
|
||||
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC
|
||||
|
||||
Reference in New Issue
Block a user