mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
42 Commits
v0.19.1
...
1365-creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da0e726326 | ||
|
|
c89f957133 | ||
|
|
8ba3a42c1e | ||
|
|
2c3ff5794d | ||
|
|
673e0a6880 | ||
|
|
b64ddf1119 | ||
|
|
2f074ac734 | ||
|
|
96e3721b4b | ||
|
|
b8e5cae88f | ||
|
|
fa20444a14 | ||
|
|
668ccabec8 | ||
|
|
aa07a0c574 | ||
|
|
0b64b43376 | ||
|
|
5c65dc9a21 | ||
|
|
58262606d4 | ||
|
|
f73959db41 | ||
|
|
e6c664e65f | ||
|
|
36cc157566 | ||
|
|
7e070623cc | ||
|
|
b2c0a685f8 | ||
|
|
c14528886d | ||
|
|
29eb490e2d | ||
|
|
6166963b00 | ||
|
|
f544efed35 | ||
|
|
598d095241 | ||
|
|
457a8e05fd | ||
|
|
3ca057c44a | ||
|
|
ad3a0198e9 | ||
|
|
ab5f62604c | ||
|
|
bf9e886b9a | ||
|
|
f5cd0fbdd8 | ||
|
|
8859cc97b4 | ||
|
|
3bdd5e4dd0 | ||
|
|
b0c710aa92 | ||
|
|
c83d0a95b7 | ||
|
|
71ca5babfd | ||
|
|
f342613503 | ||
|
|
efd176451f | ||
|
|
a7fd64e019 | ||
|
|
21c8b98f9c | ||
|
|
6846e0e5a3 | ||
|
|
49d4cea06f |
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a bug report
|
description: Create a bug report
|
||||||
labels: ["bug"]
|
labels: ["needs-triage🔍"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -62,6 +62,7 @@ body:
|
|||||||
- "Docker"
|
- "Docker"
|
||||||
- "Remote server"
|
- "Remote server"
|
||||||
- "Local Development"
|
- "Local Development"
|
||||||
|
- "Cloud Version"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const baseAdmin: User = {
|
|||||||
letsEncryptEmail: null,
|
letsEncryptEmail: null,
|
||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
logCleanupCron: null,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
}, [appName]);
|
}, [appName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
|
<div className="rounded-xl bg-background flex flex-col gap-4">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
||||||
|
|||||||
@@ -383,9 +383,8 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
side="top"
|
side="top"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
If ot server is selected, the application
|
If no server is selected, the application will be
|
||||||
will be deployed on the server where the
|
deployed on the server where the user is logged in.
|
||||||
user is logged in.
|
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@@ -14,6 +14,13 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
|
export interface RequestDistributionChartProps {
|
||||||
|
dateRange?: {
|
||||||
|
from: Date | undefined;
|
||||||
|
to: Date | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
views: {
|
views: {
|
||||||
label: "Page Views",
|
label: "Page Views",
|
||||||
@@ -24,10 +31,22 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const RequestDistributionChart = () => {
|
export const RequestDistributionChart = ({
|
||||||
const { data: stats } = api.settings.readStats.useQuery(undefined, {
|
dateRange,
|
||||||
refetchInterval: 1333,
|
}: RequestDistributionChartProps) => {
|
||||||
});
|
const { data: stats } = api.settings.readStats.useQuery(
|
||||||
|
{
|
||||||
|
dateRange: dateRange
|
||||||
|
? {
|
||||||
|
start: dateRange.from?.toISOString(),
|
||||||
|
end: dateRange.to?.toISOString(),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchInterval: 1333,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
|||||||
@@ -79,7 +79,15 @@ export const priorities = [
|
|||||||
icon: Server,
|
icon: Server,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export const RequestsTable = () => {
|
|
||||||
|
export interface RequestsTableProps {
|
||||||
|
dateRange?: {
|
||||||
|
from: Date | undefined;
|
||||||
|
to: Date | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
||||||
const [statusFilter, setStatusFilter] = useState<string[]>([]);
|
const [statusFilter, setStatusFilter] = useState<string[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedRow, setSelectedRow] = useState<LogEntry>();
|
const [selectedRow, setSelectedRow] = useState<LogEntry>();
|
||||||
@@ -98,6 +106,12 @@ export const RequestsTable = () => {
|
|||||||
page: pagination,
|
page: pagination,
|
||||||
search,
|
search,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
|
dateRange: dateRange
|
||||||
|
? {
|
||||||
|
start: dateRange.from?.toISOString(),
|
||||||
|
end: dateRange.to?.toISOString(),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchInterval: 1333,
|
refetchInterval: 1333,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -8,9 +9,29 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { ArrowDownUp } from "lucide-react";
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
ArrowDownUp,
|
||||||
|
AlertCircle,
|
||||||
|
InfoIcon,
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||||
import { RequestsTable } from "./requests-table";
|
import { RequestsTable } from "./requests-table";
|
||||||
@@ -20,17 +41,30 @@ export type LogEntry = NonNullable<
|
|||||||
>[0];
|
>[0];
|
||||||
|
|
||||||
export const ShowRequests = () => {
|
export const ShowRequests = () => {
|
||||||
const { data: isLogRotateActive, refetch: refetchLogRotate } =
|
|
||||||
api.settings.getLogRotateStatus.useQuery();
|
|
||||||
|
|
||||||
const { mutateAsync: toggleLogRotate } =
|
|
||||||
api.settings.toggleLogRotate.useMutation();
|
|
||||||
|
|
||||||
const { data: isActive, refetch } =
|
const { data: isActive, refetch } =
|
||||||
api.settings.haveActivateRequests.useQuery();
|
api.settings.haveActivateRequests.useQuery();
|
||||||
const { mutateAsync: toggleRequests } =
|
const { mutateAsync: toggleRequests } =
|
||||||
api.settings.toggleRequests.useMutation();
|
api.settings.toggleRequests.useMutation();
|
||||||
|
|
||||||
|
const { data: logCleanupStatus } =
|
||||||
|
api.settings.getLogCleanupStatus.useQuery();
|
||||||
|
const { mutateAsync: updateLogCleanup } =
|
||||||
|
api.settings.updateLogCleanup.useMutation();
|
||||||
|
const [cronExpression, setCronExpression] = useState<string | null>(null);
|
||||||
|
const [dateRange, setDateRange] = useState<{
|
||||||
|
from: Date | undefined;
|
||||||
|
to: Date | undefined;
|
||||||
|
}>({
|
||||||
|
from: undefined,
|
||||||
|
to: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logCleanupStatus) {
|
||||||
|
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
|
||||||
|
}
|
||||||
|
}, [logCleanupStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -57,7 +91,60 @@ export const ShowRequests = () => {
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
<div className="flex w-full gap-4 justify-end">
|
<div className="flex w-full gap-4 justify-end items-center">
|
||||||
|
<div className="flex-1 flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label htmlFor="cron" className="min-w-32">
|
||||||
|
Log Cleanup Schedule
|
||||||
|
</Label>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="size-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="max-w-80">
|
||||||
|
At the scheduled time, the cleanup job will keep
|
||||||
|
only the last 1000 entries in the access log file
|
||||||
|
and signal Traefik to reopen its log files. The
|
||||||
|
default schedule is daily at midnight (0 0 * * *).
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex gap-4">
|
||||||
|
<Input
|
||||||
|
id="cron"
|
||||||
|
placeholder="0 0 * * *"
|
||||||
|
value={cronExpression || ""}
|
||||||
|
onChange={(e) => setCronExpression(e.target.value)}
|
||||||
|
className="max-w-60"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!cronExpression?.trim()) {
|
||||||
|
toast.error("Please enter a valid cron expression");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateLogCleanup({
|
||||||
|
cronExpression: cronExpression,
|
||||||
|
});
|
||||||
|
toast.success("Log cleanup schedule updated");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Failed to update log cleanup schedule: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update Schedule
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title={isActive ? "Deactivate Requests" : "Activate Requests"}
|
title={isActive ? "Deactivate Requests" : "Activate Requests"}
|
||||||
description="You will also need to restart Traefik to apply the changes"
|
description="You will also need to restart Traefik to apply the changes"
|
||||||
@@ -77,53 +164,81 @@ export const ShowRequests = () => {
|
|||||||
>
|
>
|
||||||
<Button>{isActive ? "Deactivate" : "Activate"}</Button>
|
<Button>{isActive ? "Deactivate" : "Activate"}</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title={
|
|
||||||
isLogRotateActive
|
|
||||||
? "Activate Log Rotate"
|
|
||||||
: "Deactivate Log Rotate"
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
isLogRotateActive
|
|
||||||
? "This will make the logs rotate on interval 1 day and maximum size of 100 MB and maximum 6 logs"
|
|
||||||
: "The log rotation will be disabled"
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
toggleLogRotate({
|
|
||||||
enable: !isLogRotateActive,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
`Log rotate ${isLogRotateActive ? "activated" : "deactivated"}`,
|
|
||||||
);
|
|
||||||
refetchLogRotate();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(err.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary">
|
|
||||||
{isLogRotateActive
|
|
||||||
? "Activate Log Rotate"
|
|
||||||
: "Deactivate Log Rotate"}
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{isActive ? (
|
||||||
{isActive ? (
|
<>
|
||||||
<RequestDistributionChart />
|
<div className="flex justify-end mb-4 gap-2">
|
||||||
) : (
|
{(dateRange.from || dateRange.to) && (
|
||||||
<div className="flex items-center justify-center min-h-[25vh]">
|
<Button
|
||||||
<span className="text-muted-foreground py-6">
|
variant="outline"
|
||||||
You need to activate requests
|
onClick={() =>
|
||||||
</span>
|
setDateRange({ from: undefined, to: undefined })
|
||||||
|
}
|
||||||
|
className="px-3"
|
||||||
|
>
|
||||||
|
Clear dates
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-[300px] justify-start text-left font-normal"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{dateRange.from ? (
|
||||||
|
dateRange.to ? (
|
||||||
|
<>
|
||||||
|
{format(dateRange.from, "LLL dd, y")} -{" "}
|
||||||
|
{format(dateRange.to, "LLL dd, y")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
format(dateRange.from, "LLL dd, y")
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span>Pick a date range</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="end">
|
||||||
|
<Calendar
|
||||||
|
initialFocus
|
||||||
|
mode="range"
|
||||||
|
defaultMonth={dateRange.from}
|
||||||
|
selected={{
|
||||||
|
from: dateRange.from,
|
||||||
|
to: dateRange.to,
|
||||||
|
}}
|
||||||
|
onSelect={(range) => {
|
||||||
|
setDateRange({
|
||||||
|
from: range?.from,
|
||||||
|
to: range?.to,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
numberOfMonths={2}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<RequestDistributionChart dateRange={dateRange} />
|
||||||
{isActive && <RequestsTable />}
|
<RequestsTable dateRange={dateRange} />
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 gap-4 text-muted-foreground">
|
||||||
|
<AlertCircle className="size-12 text-muted-foreground/50" />
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">
|
||||||
|
Requests are not activated
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm max-w-md">
|
||||||
|
Activate requests to see incoming traffic statistics and
|
||||||
|
monitor your application's usage. After activation, you'll
|
||||||
|
need to reload Traefik for the changes to take effect.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ import { S3_PROVIDERS } from "./constants";
|
|||||||
|
|
||||||
const addDestination = z.object({
|
const addDestination = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
provider: z.string().optional(),
|
provider: z.string().min(1, "Provider is required"),
|
||||||
accessKeyId: z.string(),
|
accessKeyId: z.string().min(1, "Access Key Id is required"),
|
||||||
secretAccessKey: z.string(),
|
secretAccessKey: z.string().min(1, "Secret Access Key is required"),
|
||||||
bucket: z.string(),
|
bucket: z.string().min(1, "Bucket is required"),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string().min(1, "Endpoint is required"),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +129,63 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTestConnection = async (serverId?: string) => {
|
||||||
|
const result = await form.trigger([
|
||||||
|
"provider",
|
||||||
|
"accessKeyId",
|
||||||
|
"secretAccessKey",
|
||||||
|
"bucket",
|
||||||
|
"endpoint",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const errors = form.formState.errors;
|
||||||
|
const errorFields = Object.entries(errors)
|
||||||
|
.map(([field, error]) => `${field}: ${error?.message}`)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
toast.error("Please fill all required fields", {
|
||||||
|
description: errorFields,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloud && !serverId) {
|
||||||
|
toast.error("Please select a server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = form.getValues("provider");
|
||||||
|
const accessKey = form.getValues("accessKeyId");
|
||||||
|
const secretKey = form.getValues("secretAccessKey");
|
||||||
|
const bucket = form.getValues("bucket");
|
||||||
|
const endpoint = form.getValues("endpoint");
|
||||||
|
const region = form.getValues("region");
|
||||||
|
|
||||||
|
const connectionString = `:s3,provider=${provider},access_key_id=${accessKey},secret_access_key=${secretKey},endpoint=${endpoint}${region ? `,region=${region}` : ""}:${bucket}`;
|
||||||
|
|
||||||
|
await testConnection({
|
||||||
|
provider,
|
||||||
|
accessKey,
|
||||||
|
bucket,
|
||||||
|
endpoint,
|
||||||
|
name: "Test",
|
||||||
|
region,
|
||||||
|
secretAccessKey: secretKey,
|
||||||
|
serverId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error connecting to provider", {
|
||||||
|
description: `${e.message}\n\nTry manually: rclone ls ${connectionString}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
@@ -349,26 +406,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
isLoading={isLoading}
|
isLoading={isLoadingConnection}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await handleTestConnection(form.getValues("serverId"));
|
||||||
provider: form.getValues("provider") || "",
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
serverId: form.getValues("serverId"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error("Error connecting the provider", {
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Test Connection
|
Test Connection
|
||||||
@@ -380,21 +420,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await testConnection({
|
await handleTestConnection();
|
||||||
provider: form.getValues("provider") || "",
|
|
||||||
accessKey: form.getValues("accessKeyId"),
|
|
||||||
bucket: form.getValues("bucket"),
|
|
||||||
endpoint: form.getValues("endpoint"),
|
|
||||||
name: "Test",
|
|
||||||
region: form.getValues("region"),
|
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Connection Success");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error connecting the provider");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Test connection
|
Test connection
|
||||||
|
|||||||
@@ -56,9 +56,17 @@ export const ShowDestinations = () => {
|
|||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
<span className="text-sm">
|
<div className="flex flex-col gap-1">
|
||||||
{index + 1}. {destination.name}
|
<span className="text-sm">
|
||||||
</span>
|
{index + 1}. {destination.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Created at:{" "}
|
||||||
|
{new Date(
|
||||||
|
destination.createdAt,
|
||||||
|
).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<HandleDestinations
|
<HandleDestinations
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
|
|||||||
@@ -45,21 +45,12 @@ const Schema = z.object({
|
|||||||
|
|
||||||
type Schema = z.infer<typeof Schema>;
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
interface Model {
|
|
||||||
id: string;
|
|
||||||
object: string;
|
|
||||||
created: number;
|
|
||||||
owned_by: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
aiId?: string;
|
aiId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HandleAi = ({ aiId }: Props) => {
|
export const HandleAi = ({ aiId }: Props) => {
|
||||||
const [models, setModels] = useState<Model[]>([]);
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { data, refetch } = api.ai.one.useQuery(
|
const { data, refetch } = api.ai.one.useQuery(
|
||||||
@@ -73,6 +64,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
const { mutateAsync, isLoading } = aiId
|
const { mutateAsync, isLoading } = aiId
|
||||||
? api.ai.update.useMutation()
|
? api.ai.update.useMutation()
|
||||||
: api.ai.create.useMutation();
|
: api.ai.create.useMutation();
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -94,50 +86,33 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
});
|
});
|
||||||
}, [aiId, form, data]);
|
}, [aiId, form, data]);
|
||||||
|
|
||||||
const fetchModels = async (apiUrl: string, apiKey: string) => {
|
const apiUrl = form.watch("apiUrl");
|
||||||
setIsLoadingModels(true);
|
const apiKey = form.watch("apiKey");
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${apiUrl}/models`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch models");
|
|
||||||
}
|
|
||||||
const res = await response.json();
|
|
||||||
setModels(res.data);
|
|
||||||
|
|
||||||
// Set default model to gpt-4 if present
|
const { data: models, isLoading: isLoadingServerModels } =
|
||||||
const defaultModel = res.data.find(
|
api.ai.getModels.useQuery(
|
||||||
(model: Model) => model.id === "gpt-4",
|
{
|
||||||
);
|
apiUrl: apiUrl ?? "",
|
||||||
if (defaultModel) {
|
apiKey: apiKey ?? "",
|
||||||
form.setValue("model", defaultModel.id);
|
},
|
||||||
return defaultModel.id;
|
{
|
||||||
}
|
enabled: !!apiUrl && !!apiKey,
|
||||||
} catch (error) {
|
onError: (error) => {
|
||||||
setError("Failed to fetch models. Please check your API URL and Key.");
|
setError(`Failed to fetch models: ${error.message}`);
|
||||||
setModels([]);
|
},
|
||||||
} finally {
|
},
|
||||||
setIsLoadingModels(false);
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const apiUrl = form.watch("apiUrl");
|
const apiUrl = form.watch("apiUrl");
|
||||||
const apiKey = form.watch("apiKey");
|
const apiKey = form.watch("apiKey");
|
||||||
if (apiUrl && apiKey) {
|
if (apiUrl && apiKey) {
|
||||||
form.setValue("model", "");
|
form.setValue("model", "");
|
||||||
fetchModels(apiUrl, apiKey);
|
|
||||||
}
|
}
|
||||||
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
try {
|
try {
|
||||||
console.log("Form data:", data);
|
|
||||||
console.log("Current model value:", form.getValues("model"));
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
...data,
|
...data,
|
||||||
aiId: aiId || "",
|
aiId: aiId || "",
|
||||||
@@ -148,8 +123,9 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Submit error:", error);
|
toast.error("Failed to save AI settings", {
|
||||||
toast.error("Failed to save AI settings");
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,13 +208,13 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoadingModels && (
|
{isLoadingServerModels && (
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Loading models...
|
Loading models...
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoadingModels && models.length > 0 && (
|
{!isLoadingServerModels && models && models.length > 0 && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="model"
|
name="model"
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { IUpdateData } from "@dokploy/server/index";
|
import type { IUpdateData } from "@dokploy/server/index";
|
||||||
import {
|
import {
|
||||||
@@ -24,9 +30,17 @@ import { UpdateWebServer } from "./update-webserver";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
updateData?: IUpdateData;
|
updateData?: IUpdateData;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateServer = ({ updateData }: Props) => {
|
export const UpdateServer = ({
|
||||||
|
updateData,
|
||||||
|
children,
|
||||||
|
isOpen: isOpenProp,
|
||||||
|
onOpenChange: onOpenChangeProp,
|
||||||
|
}: Props) => {
|
||||||
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
|
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
|
||||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
||||||
!!updateData?.updateAvailable,
|
!!updateData?.updateAvailable,
|
||||||
@@ -35,10 +49,10 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
api.settings.getUpdateData.useMutation();
|
api.settings.getUpdateData.useMutation();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
|
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [latestVersion, setLatestVersion] = useState(
|
const [latestVersion, setLatestVersion] = useState(
|
||||||
updateData?.latestVersion ?? "",
|
updateData?.latestVersion ?? "",
|
||||||
);
|
);
|
||||||
|
const [isOpenInternal, setIsOpenInternal] = useState(false);
|
||||||
|
|
||||||
const handleCheckUpdates = async () => {
|
const handleCheckUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -65,28 +79,52 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOpen = isOpenInternal || isOpenProp;
|
||||||
|
const onOpenChange = (open: boolean) => {
|
||||||
|
setIsOpenInternal(open);
|
||||||
|
onOpenChangeProp?.(open);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
{children ? (
|
||||||
variant={updateData ? "outline" : "secondary"}
|
children
|
||||||
className="gap-2"
|
) : (
|
||||||
>
|
<TooltipProvider delayDuration={0}>
|
||||||
{updateData ? (
|
<Tooltip>
|
||||||
<>
|
<TooltipTrigger asChild>
|
||||||
<span className="flex h-2 w-2">
|
<Button
|
||||||
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
|
variant={updateData ? "outline" : "secondary"}
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
size="sm"
|
||||||
</span>
|
onClick={() => onOpenChange?.(true)}
|
||||||
Update available
|
>
|
||||||
</>
|
<Download className="h-4 w-4 flex-shrink-0" />
|
||||||
) : (
|
{updateData ? (
|
||||||
<>
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
<Sparkles className="h-4 w-4" />
|
Update Available
|
||||||
Updates
|
</span>
|
||||||
</>
|
) : (
|
||||||
)}
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
</Button>
|
Check for updates
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{updateData && (
|
||||||
|
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{updateData && (
|
||||||
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
|
<p>Update Available</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-lg p-6">
|
<DialogContent className="max-w-lg p-6">
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
@@ -217,7 +255,7 @@ export const UpdateServer = ({ updateData }: Props) => {
|
|||||||
|
|
||||||
<div className="space-y-4 flex items-center justify-end">
|
<div className="space-y-4 flex items-center justify-end">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{isUpdateAvailable ? (
|
{isUpdateAvailable ? (
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@@ -1017,18 +1015,16 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
{!isCloud && auth?.role === "owner" && (
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton asChild>
|
|
||||||
<UpdateServerButton />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
)}
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu className="flex flex-col gap-2">
|
||||||
|
{!isCloud && auth?.role === "owner" && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<UpdateServerButton />
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)}
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -1055,10 +1051,6 @@ export default function Page({ children }: Props) {
|
|||||||
</Link>
|
</Link>
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="block" />
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<BreadcrumbPage>{activeItem?.title}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ import type { IUpdateData } from "@dokploy/server/index";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "../ui/tooltip";
|
||||||
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||||
|
|
||||||
export const UpdateServerButton = () => {
|
export const UpdateServerButton = () => {
|
||||||
@@ -15,6 +22,7 @@ export const UpdateServerButton = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { mutateAsync: getUpdateData } =
|
const { mutateAsync: getUpdateData } =
|
||||||
api.settings.getUpdateData.useMutation();
|
api.settings.getUpdateData.useMutation();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
||||||
|
|
||||||
@@ -69,11 +77,47 @@ export const UpdateServerButton = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return updateData.updateAvailable ? (
|
||||||
updateData.updateAvailable && (
|
<div className="border-t pt-4">
|
||||||
<div>
|
<UpdateServer
|
||||||
<UpdateServer updateData={updateData} />
|
updateData={updateData}
|
||||||
</div>
|
isOpen={isOpen}
|
||||||
)
|
onOpenChange={setIsOpen}
|
||||||
);
|
>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={updateData ? "outline" : "secondary"}
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 flex-shrink-0" />
|
||||||
|
{updateData ? (
|
||||||
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
|
Update Available
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
||||||
|
Check for updates
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{updateData && (
|
||||||
|
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{updateData && (
|
||||||
|
<TooltipContent side="right" sideOffset={10}>
|
||||||
|
<p>Update Available</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</UpdateServer>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="block" />
|
{_index + 1 < list.length && <BreadcrumbSeparator className="block" />}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
|
|||||||
68
apps/dokploy/components/ui/calendar.tsx
Normal file
68
apps/dokploy/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import type * as React from "react";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { DayPicker } from "react-day-picker";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
...props
|
||||||
|
}: CalendarProps) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||||
|
month: "space-y-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "space-x-1 flex items-center",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-y-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell:
|
||||||
|
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
|
day: cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
||||||
|
),
|
||||||
|
day_range_end: "day-range-end",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside:
|
||||||
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle:
|
||||||
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
IconLeft: ({ className, ...props }) => (
|
||||||
|
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
IconRight: ({ className, ...props }) => (
|
||||||
|
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
|
export { Calendar };
|
||||||
1
apps/dokploy/drizzle/0070_useful_serpent_society.sql
Normal file
1
apps/dokploy/drizzle/0070_useful_serpent_society.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "destination" ADD COLUMN "createdAt" timestamp DEFAULT now() NOT NULL;
|
||||||
1
apps/dokploy/drizzle/0071_flaky_black_queen.sql
Normal file
1
apps/dokploy/drizzle/0071_flaky_black_queen.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "logCleanupCron" text;
|
||||||
5126
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
5126
apps/dokploy/drizzle/meta/0070_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5132
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
5132
apps/dokploy/drizzle/meta/0071_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -491,6 +491,20 @@
|
|||||||
"when": 1741152916611,
|
"when": 1741152916611,
|
||||||
"tag": "0069_legal_bill_hollister",
|
"tag": "0069_legal_bill_hollister",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741322697251,
|
||||||
|
"tag": "0070_useful_serpent_society",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 71,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741460060541,
|
||||||
|
"tag": "0071_flaky_black_queen",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -228,15 +228,15 @@ const Service = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="preview-deployments">
|
<TabsTrigger value="preview-deployments">
|
||||||
Preview Deployments
|
Preview Deployments
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -224,12 +224,12 @@ const Service = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,11 +197,11 @@ const Mariadb = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -198,11 +198,11 @@ const Mongo = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -200,13 +200,13 @@ const MySql = (
|
|||||||
<TabsTrigger value="environment">
|
<TabsTrigger value="environment">
|
||||||
Environment
|
Environment
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">
|
<TabsTrigger value="monitoring">
|
||||||
Monitoring
|
Monitoring
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,11 +197,11 @@ const Postgresql = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,10 +197,10 @@ const Redis = (
|
|||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
28
apps/dokploy/public/templates/datalens.svg
Normal file
28
apps/dokploy/public/templates/datalens.svg
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="163" height="32" fill="none" viewBox="0 0 163 32">
|
||||||
|
<g clip-path="url(#a)">
|
||||||
|
<rect width="32" height="32" fill="#FF6928" rx="16" />
|
||||||
|
<path fill="#fff" fill-rule="evenodd" d="M7.556 29.778C-3 29.778-11.556 23.609-11.556 16c0-7.61 8.557-13.778 19.112-13.778 11.17 0 19.11 5.905 19.11 13.778s-7.94 13.778-19.11 13.778Zm0-8c-10.17 0-17.334-2.587-17.334-5.778 0-3.191 7.163-5.778 17.334-5.778 10.17 0 17.333 2.128 17.333 5.778 0 3.65-7.163 5.778-17.333 5.778Z" clip-rule="evenodd" />
|
||||||
|
<g filter="url(#b)" opacity=".3">
|
||||||
|
<path fill="#FF6928" d="M1.893 11.25C4.192 6.285 5.753 3.667 10.495.177L7.9-5.684-5.308-2.17l-.438 15.1 7.639-1.678Z" />
|
||||||
|
</g>
|
||||||
|
<g filter="url(#c)" opacity=".3">
|
||||||
|
<path fill="#FF6928" d="M12.434 29.6c-4.607-2.955-6.988-4.86-9.798-10.033l-6.16 1.773 1.679 13.563 14.898 2.493-.62-7.796Z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path fill="currentColor" d="M46.18 22.416c4.02 0 6.03-2.171 6.03-6.514v-.227c0-2.143-.493-3.745-1.48-4.807-.966-1.081-2.502-1.622-4.607-1.622h-1.82v13.17h1.877ZM39.694 5.662h6.656c3.47 0 6.144.892 8.022 2.674 1.763 1.764 2.645 4.19 2.645 7.282v.227c0 3.13-.891 5.585-2.674 7.367C52.485 25.071 49.811 26 46.322 26h-6.628V5.662Zm23.788 20.65c-1.441 0-2.608-.35-3.499-1.052-.986-.777-1.479-1.905-1.479-3.384 0-1.65.72-2.883 2.162-3.698 1.29-.72 3.148-1.081 5.575-1.081h1.678V16.5c0-.949-.18-1.64-.54-2.077-.342-.436-.968-.654-1.878-.654-1.46 0-2.304.701-2.531 2.105h-3.897c.114-1.669.806-2.958 2.076-3.869 1.157-.815 2.693-1.223 4.608-1.223 1.916 0 3.414.417 4.494 1.252 1.157.91 1.736 2.332 1.736 4.266V26h-4.011v-1.792c-1.005 1.403-2.503 2.105-4.494 2.105Zm1.223-2.872c.93 0 1.697-.237 2.304-.711.607-.474.91-1.119.91-1.934v-1.252h-1.593c-1.251 0-2.2.161-2.844.484-.626.322-.939.863-.939 1.621 0 1.195.72 1.792 2.162 1.792Zm15.734 2.844c-3.204 0-4.807-1.564-4.807-4.693v-7.538h-1.905v-2.93h1.905V7.91h4.096v3.215h3.13v2.93h-3.13v7.167c0 1.176.55 1.764 1.65 1.764.607 0 1.129-.095 1.565-.285v3.186c-.759.266-1.593.398-2.504.398Zm8.92.029c-1.442 0-2.608-.35-3.5-1.053-.985-.777-1.478-1.905-1.478-3.384 0-1.65.72-2.883 2.161-3.698 1.29-.72 3.148-1.081 5.576-1.081h1.678V16.5c0-.949-.18-1.64-.54-2.077-.342-.436-.968-.654-1.878-.654-1.46 0-2.304.701-2.532 2.105H84.95c.114-1.669.806-2.958 2.077-3.869 1.157-.815 2.693-1.223 4.608-1.223 1.915 0 3.413.417 4.494 1.252 1.157.91 1.735 2.332 1.735 4.266V26h-4.01v-1.792c-1.005 1.403-2.503 2.105-4.495 2.105Zm1.223-2.873c.929 0 1.697-.237 2.303-.711.607-.474.91-1.119.91-1.934v-1.252h-1.592c-1.252 0-2.2.161-2.845.484-.625.322-.938.863-.938 1.621 0 1.195.72 1.792 2.162 1.792Zm10.671-17.778h4.608v16.726h8.277V26h-12.885V5.662Zm21.632 20.65c-2.313 0-4.162-.672-5.547-2.019-1.479-1.365-2.218-3.214-2.218-5.546v-.228c0-2.313.739-4.19 2.218-5.632 1.442-1.403 3.252-2.105 5.433-2.105 2.067 0 3.755.598 5.063 1.792 1.46 1.328 2.191 3.252 2.191 5.775v1.137h-10.724c.057 1.252.398 2.219 1.024 2.902.645.663 1.536.995 2.674.995 1.782 0 2.816-.692 3.1-2.076h3.897c-.246 1.612-.986 2.854-2.219 3.726-1.213.853-2.844 1.28-4.892 1.28Zm3.129-9.357c-.133-2.219-1.214-3.328-3.243-3.328-.929 0-1.697.294-2.304.881-.588.57-.957 1.385-1.109 2.447h6.656Zm6.19-5.831h4.125v2.36c.929-1.801 2.541-2.702 4.835-2.702 1.498 0 2.703.455 3.613 1.366.929.986 1.394 2.446 1.394 4.38V26h-4.125v-8.875c0-1.024-.209-1.773-.626-2.247-.417-.493-1.081-.74-1.991-.74-.929 0-1.678.285-2.247.854-.569.55-.853 1.356-.853 2.418V26h-4.125V11.124Zm22.385 15.189c-2.01 0-3.575-.427-4.694-1.28-1.118-.853-1.716-2.086-1.792-3.698h3.84c.114.72.361 1.252.74 1.593.379.341 1.005.512 1.877.512 1.555 0 2.333-.54 2.333-1.621 0-.512-.228-.892-.683-1.138-.455-.266-1.233-.474-2.332-.626-2.01-.322-3.414-.806-4.21-1.45-.853-.664-1.28-1.726-1.28-3.186s.607-2.627 1.82-3.499c1.043-.758 2.399-1.138 4.068-1.138 1.725 0 3.119.342 4.181 1.024 1.119.759 1.773 1.953 1.963 3.584h-3.783c-.114-.626-.351-1.08-.711-1.365-.361-.284-.901-.427-1.622-.427-.663 0-1.185.143-1.564.427a1.35 1.35 0 0 0-.541 1.11c0 .473.2.824.598 1.052.417.227 1.175.417 2.275.569 1.953.265 3.385.71 4.295 1.337.986.739 1.48 1.848 1.48 3.328 0 1.592-.55 2.806-1.65 3.64-1.081.835-2.617 1.252-4.608 1.252Z" />
|
||||||
|
<defs>
|
||||||
|
<filter id="b" width="37.575" height="39.945" x="-16.413" y="-16.35" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feGaussianBlur result="effect1_foregroundBlur_219_14417" stdDeviation="5.333" />
|
||||||
|
</filter>
|
||||||
|
<filter id="c" width="37.91" height="39.162" x="-14.19" y="8.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||||
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||||
|
<feGaussianBlur result="effect1_foregroundBlur_219_14417" stdDeviation="5.333" />
|
||||||
|
</filter>
|
||||||
|
<clipPath id="a">
|
||||||
|
<rect width="32" height="32" fill="#fff" rx="16" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
7
apps/dokploy/public/templates/hoarder.svg
Normal file
7
apps/dokploy/public/templates/hoarder.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 355 354" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(1,0,0,1,-232,-118)">
|
||||||
|
<path d="M565.33,118.79L253.02,118.79C241.46,118.79 232.09,128.16 232.09,139.72L232.09,450.44C232.09,462 241.46,471.37 253.02,471.37L565.33,471.37C576.89,471.37 586.26,462 586.26,450.44L586.26,139.72C586.26,128.16 576.89,118.79 565.33,118.79ZM386.85,419.57C386.85,422.58 384.4,425.03 381.39,425.03L285.11,425.03C282.1,425.03 279.65,422.58 279.65,419.57L279.65,169.43C279.65,166.42 282.1,163.96 285.11,163.96L379.87,163.96C382.88,163.96 385.33,166.41 385.33,169.43L385.33,264.64C385.33,264.64 384.81,305.61 386.85,337.76L386.85,419.58L386.85,419.57ZM537.19,419.57C537.19,423.92 532.36,426.52 528.75,424.14L484.85,395.44C482.95,394.18 480.5,394.25 478.64,395.59L440.16,423.4C438.56,424.59 436.67,424.66 435.07,424.07C433.66,423.07 432.73,421.43 432.73,419.57L432.73,225.34C437.94,224.34 443.73,223.78 450.43,223.78C483.29,223.78 537.2,242.37 537.2,294.78L537.2,419.58L537.19,419.57Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -25,6 +25,10 @@ import {
|
|||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/user";
|
||||||
|
import {
|
||||||
|
getProviderHeaders,
|
||||||
|
type Model,
|
||||||
|
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -41,6 +45,58 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return aiSetting;
|
return aiSetting;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getModels: protectedProcedure
|
||||||
|
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
||||||
|
const response = await fetch(`${input.apiUrl}/models`, { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Failed to fetch models: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
return res.map((model) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.models) {
|
||||||
|
return res.models.map((model: any) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
})) as Model[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data) {
|
||||||
|
return res.data as Model[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const possibleModels =
|
||||||
|
(Object.values(res).find(Array.isArray) as any[]) || [];
|
||||||
|
return possibleModels.map((model) => ({
|
||||||
|
id: model.id || model.name,
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "provider",
|
||||||
|
})) as Model[];
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: error instanceof Error ? error?.message : `Error: ${error}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
updateDestinationById,
|
updateDestinationById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, desc } from "drizzle-orm";
|
||||||
|
|
||||||
export const destinationRouter = createTRPCRouter({
|
export const destinationRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
@@ -98,6 +98,7 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.destinations.findMany({
|
return await db.query.destinations.findMany({
|
||||||
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
orderBy: [desc(destinations.createdAt)],
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: adminProcedure
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
import { db } from "@/server/db";
|
||||||
|
import { apiFindAllByApplication, applications } from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
createPreviewDeployment,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
|
findPreviewDeploymentByApplicationId,
|
||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
findPreviewDeploymentsByApplicationId,
|
findPreviewDeploymentsByApplicationId,
|
||||||
|
findPreviewDeploymentsByPullRequestId,
|
||||||
|
IS_CLOUD,
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { and } from "drizzle-orm";
|
||||||
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
|
import { deploy } from "@/server/utils/deploy";
|
||||||
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
|
|
||||||
export const previewDeploymentRouter = createTRPCRouter({
|
export const previewDeploymentRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return previewDeployment;
|
return previewDeployment;
|
||||||
}),
|
}),
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
|
||||||
|
pullRequestId: z.string(),
|
||||||
|
repository: z.string(),
|
||||||
|
owner: z.string(),
|
||||||
|
branch: z.string(),
|
||||||
|
deploymentHash: z.string(),
|
||||||
|
prBranch: z.string(),
|
||||||
|
prNumber: z.any(),
|
||||||
|
prTitle: z.string(),
|
||||||
|
prURL: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const organizationId = ctx.session.activeOrganizationId;
|
||||||
|
const action = input.action;
|
||||||
|
const prId = input.pullRequestId;
|
||||||
|
|
||||||
|
if (action === "closed") {
|
||||||
|
const previewDeploymentResult =
|
||||||
|
await findPreviewDeploymentsByPullRequestId(prId);
|
||||||
|
|
||||||
|
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
|
||||||
|
(previewDeployment) =>
|
||||||
|
previewDeployment.application.project.organizationId ===
|
||||||
|
organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredPreviewDeploymentResult.length > 0) {
|
||||||
|
for (const previewDeployment of filteredPreviewDeploymentResult) {
|
||||||
|
try {
|
||||||
|
await removePreviewDeployment(
|
||||||
|
previewDeployment.previewDeploymentId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Preview Deployments Closed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action === "opened" ||
|
||||||
|
action === "synchronize" ||
|
||||||
|
action === "reopened"
|
||||||
|
) {
|
||||||
|
const deploymentHash = input.deploymentHash;
|
||||||
|
|
||||||
|
const prBranch = input.prBranch;
|
||||||
|
const prNumber = input.prNumber;
|
||||||
|
const prTitle = input.prTitle;
|
||||||
|
const prURL = input.prURL;
|
||||||
|
const apps = await db.query.applications.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(applications.sourceType, "github"),
|
||||||
|
eq(applications.repository, input.repository),
|
||||||
|
eq(applications.branch, input.branch),
|
||||||
|
eq(applications.isPreviewDeploymentsActive, true),
|
||||||
|
eq(applications.owner, input.owner),
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
previewDeployments: true,
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredApps = apps.filter(
|
||||||
|
(app) => app.project.organizationId === organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(filteredApps);
|
||||||
|
|
||||||
|
for (const app of filteredApps) {
|
||||||
|
const previewLimit = app?.previewLimit || 0;
|
||||||
|
if (app?.previewDeployments?.length > previewLimit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previewDeploymentResult =
|
||||||
|
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
|
||||||
|
|
||||||
|
let previewDeploymentId =
|
||||||
|
previewDeploymentResult?.previewDeploymentId || "";
|
||||||
|
|
||||||
|
if (!previewDeploymentResult) {
|
||||||
|
try {
|
||||||
|
const previewDeployment = await createPreviewDeployment({
|
||||||
|
applicationId: app.applicationId as string,
|
||||||
|
branch: prBranch,
|
||||||
|
pullRequestId: prId,
|
||||||
|
pullRequestNumber: prNumber,
|
||||||
|
pullRequestTitle: prTitle,
|
||||||
|
pullRequestURL: prURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(previewDeployment);
|
||||||
|
previewDeploymentId = previewDeployment.previewDeploymentId;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
applicationId: app.applicationId as string,
|
||||||
|
titleLog: "Preview Deployment",
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "application-preview",
|
||||||
|
server: !!app.serverId,
|
||||||
|
previewDeploymentId,
|
||||||
|
isExternal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (IS_CLOUD && app.serverId) {
|
||||||
|
jobData.serverId = app.serverId;
|
||||||
|
await deploy(jobData);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Preview Deployments Created",
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
getUpdateData,
|
getUpdateData,
|
||||||
initializeTraefik,
|
initializeTraefik,
|
||||||
logRotationManager,
|
|
||||||
parseRawConfig,
|
parseRawConfig,
|
||||||
paths,
|
paths,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
@@ -53,6 +52,9 @@ import {
|
|||||||
writeConfig,
|
writeConfig,
|
||||||
writeMainConfig,
|
writeMainConfig,
|
||||||
writeTraefikConfigInPath,
|
writeTraefikConfigInPath,
|
||||||
|
startLogCleanup,
|
||||||
|
stopLogCleanup,
|
||||||
|
getLogCleanupStatus,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
||||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
@@ -577,48 +579,35 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const rawConfig = readMonitoringConfig();
|
const rawConfig = readMonitoringConfig(
|
||||||
|
!!input.dateRange?.start && !!input.dateRange?.end,
|
||||||
|
);
|
||||||
|
|
||||||
const parsedConfig = parseRawConfig(
|
const parsedConfig = parseRawConfig(
|
||||||
rawConfig as string,
|
rawConfig as string,
|
||||||
input.page,
|
input.page,
|
||||||
input.sort,
|
input.sort,
|
||||||
input.search,
|
input.search,
|
||||||
input.status,
|
input.status,
|
||||||
|
input.dateRange,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedConfig;
|
return parsedConfig;
|
||||||
}),
|
}),
|
||||||
readStats: adminProcedure.query(() => {
|
readStats: adminProcedure
|
||||||
if (IS_CLOUD) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const rawConfig = readMonitoringConfig();
|
|
||||||
const processedLogs = processLogs(rawConfig as string);
|
|
||||||
return processedLogs || [];
|
|
||||||
}),
|
|
||||||
getLogRotateStatus: adminProcedure.query(async () => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return await logRotationManager.getStatus();
|
|
||||||
}),
|
|
||||||
toggleLogRotate: adminProcedure
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
enable: z.boolean(),
|
start: z.string().optional(),
|
||||||
|
end: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.query(({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return [];
|
||||||
}
|
}
|
||||||
if (input.enable) {
|
const rawConfig = readMonitoringConfig(!!input?.start || !!input?.end);
|
||||||
await logRotationManager.activate();
|
const processedLogs = processLogs(rawConfig as string, input);
|
||||||
} else {
|
return processedLogs || [];
|
||||||
await logRotationManager.deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
}),
|
||||||
haveActivateRequests: adminProcedure.query(async () => {
|
haveActivateRequests: adminProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
@@ -820,10 +809,20 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
updateLogCleanup: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
cronExpression: z.string().nullable(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (input.cronExpression) {
|
||||||
|
return startLogCleanup(input.cronExpression);
|
||||||
|
}
|
||||||
|
return stopLogCleanup();
|
||||||
|
}),
|
||||||
|
|
||||||
|
getLogCleanupStatus: adminProcedure.query(async () => {
|
||||||
|
return getLogCleanupStatus();
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
// {
|
|
||||||
// "Parallelism": 1,
|
|
||||||
// "Delay": 10000000000,
|
|
||||||
// "FailureAction": "rollback",
|
|
||||||
// "Order": "start-first"
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
|
isExternal: job.data.isExternal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
|
isExternal: job.data.isExternal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type DeployJob =
|
|||||||
applicationType: "application-preview";
|
applicationType: "application-preview";
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
isExternal?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentJob = DeployJob;
|
export type DeploymentJob = DeployJob;
|
||||||
|
|||||||
96
apps/dokploy/templates/datalens/docker-compose.yml
Normal file
96
apps/dokploy/templates/datalens/docker-compose.yml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
services:
|
||||||
|
pg-compeng:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: "postgres"
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
|
||||||
|
control-api:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-control-api:0.2192.0
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
BI_API_UWSGI_WORKERS_COUNT: 4
|
||||||
|
CONNECTOR_AVAILABILITY_VISIBLE: "clickhouse,postgres,chyt,ydb,mysql,greenplum,mssql,appmetrica_api,metrika_api"
|
||||||
|
RQE_FORCE_OFF: 1
|
||||||
|
DL_CRY_ACTUAL_KEY_ID: key_1
|
||||||
|
DL_CRY_KEY_VAL_ID_key_1: "h1ZpilcYLYRdWp7Nk8X1M1kBPiUi8rdjz9oBfHyUKIk="
|
||||||
|
RQE_SECRET_KEY: ""
|
||||||
|
US_HOST: "http://us:8083"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
|
||||||
|
data-api:
|
||||||
|
container_name: datalens-data-api
|
||||||
|
image: ghcr.io/datalens-tech/datalens-data-api:0.2192.0
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
GUNICORN_WORKERS_COUNT: 5
|
||||||
|
RQE_FORCE_OFF: 1
|
||||||
|
CACHES_ON: 0
|
||||||
|
MUTATIONS_CACHES_ON: 0
|
||||||
|
RQE_SECRET_KEY: ""
|
||||||
|
DL_CRY_ACTUAL_KEY_ID: key_1
|
||||||
|
DL_CRY_KEY_VAL_ID_key_1: "h1ZpilcYLYRdWp7Nk8X1M1kBPiUi8rdjz9oBfHyUKIk="
|
||||||
|
BI_COMPENG_PG_ON: 1
|
||||||
|
BI_COMPENG_PG_URL: "postgresql://postgres:postgres@pg-compeng:5432/postgres"
|
||||||
|
US_HOST: "http://us:8083"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
- pg-compeng
|
||||||
|
|
||||||
|
pg-us:
|
||||||
|
container_name: datalens-pg-us
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: us-db-ci_purgeable
|
||||||
|
POSTGRES_USER: us
|
||||||
|
POSTGRES_PASSWORD: us
|
||||||
|
volumes:
|
||||||
|
- ${VOLUME_US:-./metadata}:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
us:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-us:0.310.0
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- pg-us
|
||||||
|
environment:
|
||||||
|
APP_INSTALLATION: "opensource"
|
||||||
|
APP_ENV: "prod"
|
||||||
|
MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
POSTGRES_DSN_LIST: ${METADATA_POSTGRES_DSN_LIST:-postgres://us:us@pg-us:5432/us-db-ci_purgeable}
|
||||||
|
SKIP_INSTALL_DB_EXTENSIONS: ${METADATA_SKIP_INSTALL_DB_EXTENSIONS:-0}
|
||||||
|
USE_DEMO_DATA: ${USE_DEMO_DATA:-0}
|
||||||
|
HC: ${HC:-0}
|
||||||
|
NODE_EXTRA_CA_CERTS: /certs/root.crt
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./certs:/certs
|
||||||
|
|
||||||
|
datalens:
|
||||||
|
image: ghcr.io/datalens-tech/datalens-ui:0.2601.0
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- ${UI_PORT:-8080}:8080
|
||||||
|
depends_on:
|
||||||
|
- us
|
||||||
|
- control-api
|
||||||
|
- data-api
|
||||||
|
environment:
|
||||||
|
APP_MODE: "full"
|
||||||
|
APP_ENV: "production"
|
||||||
|
APP_INSTALLATION: "opensource"
|
||||||
|
AUTH_POLICY: "disabled"
|
||||||
|
US_ENDPOINT: "http://us:8083"
|
||||||
|
BI_API_ENDPOINT: "http://control-api:8080"
|
||||||
|
BI_DATA_ENDPOINT: "http://data-api:8080"
|
||||||
|
US_MASTER_TOKEN: "fake-us-master-token"
|
||||||
|
NODE_EXTRA_CA_CERTS: "/usr/local/share/ca-certificates/cert.pem"
|
||||||
|
HC: ${HC:-0}
|
||||||
|
YANDEX_MAP_ENABLED: ${YANDEX_MAP_ENABLED:-0}
|
||||||
|
YANDEX_MAP_TOKEN: ${YANDEX_MAP_TOKEN:-0}
|
||||||
23
apps/dokploy/templates/datalens/index.ts
Normal file
23
apps/dokploy/templates/datalens/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: generateRandomDomain(schema),
|
||||||
|
port: 8080,
|
||||||
|
serviceName: "datalens",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = ["HC=1"];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
apps/dokploy/templates/hoarder/docker-compose.yml
Normal file
45
apps/dokploy/templates/hoarder/docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: ghcr.io/hoarder-app/hoarder:0.22.0
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- hoarder-data:/data
|
||||||
|
ports:
|
||||||
|
- 3000
|
||||||
|
environment:
|
||||||
|
- DISABLE_SIGNUPS
|
||||||
|
- NEXTAUTH_URL
|
||||||
|
- NEXTAUTH_SECRET
|
||||||
|
- MEILI_ADDR=http://meilisearch:7700
|
||||||
|
- BROWSER_WEB_URL=http://chrome:9222
|
||||||
|
- DATA_DIR=/data
|
||||||
|
chrome:
|
||||||
|
image: gcr.io/zenika-hub/alpine-chrome:124
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --no-sandbox
|
||||||
|
- --disable-gpu
|
||||||
|
- --disable-dev-shm-usage
|
||||||
|
- --remote-debugging-address=0.0.0.0
|
||||||
|
- --remote-debugging-port=9222
|
||||||
|
- --hide-scrollbars
|
||||||
|
meilisearch:
|
||||||
|
image: getmeili/meilisearch:v1.6
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MEILI_MASTER_KEY
|
||||||
|
- MEILI_NO_ANALYTICS="true"
|
||||||
|
volumes:
|
||||||
|
- meilisearch-data:/meili_data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- curl
|
||||||
|
- '-f'
|
||||||
|
- 'http://127.0.0.1:7700/health'
|
||||||
|
interval: 2s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 15
|
||||||
|
volumes:
|
||||||
|
meilisearch-data:
|
||||||
|
hoarder-data:
|
||||||
34
apps/dokploy/templates/hoarder/index.ts
Normal file
34
apps/dokploy/templates/hoarder/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generateBase64,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mainDomain = generateRandomDomain(schema);
|
||||||
|
const postgresPassword = generatePassword();
|
||||||
|
const nextSecret = generateBase64(32);
|
||||||
|
const meiliMasterKey = generateBase64(32);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: mainDomain,
|
||||||
|
port: 3000,
|
||||||
|
serviceName: "web",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`NEXTAUTH_SECRET=${nextSecret}`,
|
||||||
|
`MEILI_MASTER_KEY=${meiliMasterKey}`,
|
||||||
|
`NEXTAUTH_URL=http://${mainDomain}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains,
|
||||||
|
envs,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
superset_redis:
|
superset_redis:
|
||||||
image: redis
|
image: redis
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -108,6 +108,20 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["monitoring"],
|
tags: ["monitoring"],
|
||||||
load: () => import("./grafana/index").then((m) => m.generate),
|
load: () => import("./grafana/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "datalens",
|
||||||
|
name: "DataLens",
|
||||||
|
version: "1.23.0",
|
||||||
|
description: "A modern, scalable business intelligence and data visualization system.",
|
||||||
|
logo: "datalens.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/datalens-tech/datalens",
|
||||||
|
website: "https://datalens.tech/",
|
||||||
|
docs: "https://datalens.tech/docs/",
|
||||||
|
},
|
||||||
|
tags: ["analytics", "self-hosted", "bi", "monitoring"],
|
||||||
|
load: () => import("./datalens/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "directus",
|
id: "directus",
|
||||||
name: "Directus",
|
name: "Directus",
|
||||||
@@ -707,6 +721,21 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["self-hosted", "open-source", "manager"],
|
tags: ["self-hosted", "open-source", "manager"],
|
||||||
load: () => import("./hi-events/index").then((m) => m.generate),
|
load: () => import("./hi-events/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "hoarder",
|
||||||
|
name: "Hoarder",
|
||||||
|
version: "0.22.0",
|
||||||
|
description:
|
||||||
|
'Hoarder is an open source "Bookmark Everything" app that uses AI for automatically tagging the content you throw at it.',
|
||||||
|
logo: "hoarder.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/hoarder/hoarder",
|
||||||
|
website: "https://hoarder.app/",
|
||||||
|
docs: "https://docs.hoarder.app/",
|
||||||
|
},
|
||||||
|
tags: ["self-hosted", "bookmarks", "link-sharing"],
|
||||||
|
load: () => import("./hoarder/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "windows",
|
id: "windows",
|
||||||
name: "Windows (dockerized)",
|
name: "Windows (dockerized)",
|
||||||
|
|||||||
@@ -17,15 +17,17 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
zipline:
|
zipline:
|
||||||
image: ghcr.io/diced/zipline:3.7.9
|
image: ghcr.io/diced/zipline:4
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- CORE_RETURN_HTTPS=${ZIPLINE_RETURN_HTTPS}
|
- CORE_RETURN_HTTPS=${ZIPLINE_RETURN_HTTPS}
|
||||||
- CORE_SECRET=${ZIPLINE_SECRET}
|
- CORE_SECRET=${ZIPLINE_SECRET}
|
||||||
- CORE_HOST=0.0.0.0
|
- CORE_HOSTNAME=0.0.0.0
|
||||||
- CORE_PORT=${ZIPLINE_PORT}
|
- CORE_PORT=${ZIPLINE_PORT}
|
||||||
- CORE_DATABASE_URL=postgres://postgres:postgres@postgres/postgres
|
- DATABASE_URL=postgres://postgres:postgres@postgres/postgres
|
||||||
- CORE_LOGGER=${ZIPLINE_LOGGER}
|
- CORE_LOGGER=${ZIPLINE_LOGGER}
|
||||||
|
- DATASOURCE_TYPE=local
|
||||||
|
- DATASOURCE_LOCAL_DIRECTORY=./uploads
|
||||||
volumes:
|
volumes:
|
||||||
- "../files/uploads:/zipline/uploads"
|
- "../files/uploads:/zipline/uploads"
|
||||||
- "../files/public:/zipline/public"
|
- "../files/public:/zipline/public"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -21,6 +21,7 @@ export const destinations = pgTable("destination", {
|
|||||||
organizationId: text("organizationId")
|
organizationId: text("organizationId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const destinationsRelations = relations(
|
export const destinationsRelations = relations(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
letsEncryptEmail: text("letsEncryptEmail"),
|
letsEncryptEmail: text("letsEncryptEmail"),
|
||||||
sshPrivateKey: text("sshPrivateKey"),
|
sshPrivateKey: text("sshPrivateKey"),
|
||||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||||
enableLogRotation: boolean("enableLogRotation").notNull().default(false),
|
logCleanupCron: text("logCleanupCron"),
|
||||||
// Metrics
|
// Metrics
|
||||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
metricsConfig: jsonb("metricsConfig")
|
||||||
@@ -250,6 +250,12 @@ export const apiReadStatsLogs = z.object({
|
|||||||
status: z.string().array().optional(),
|
status: z.string().array().optional(),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
|
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
|
||||||
|
dateRange: z
|
||||||
|
.object({
|
||||||
|
start: z.string().optional(),
|
||||||
|
end: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUpdateWebServerMonitoring = z.object({
|
export const apiUpdateWebServerMonitoring = z.object({
|
||||||
@@ -305,4 +311,5 @@ export const apiUpdateUser = createSchema.partial().extend({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
logCleanupCron: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,3 +116,9 @@ export * from "./db/validations/index";
|
|||||||
export * from "./utils/gpu-setup";
|
export * from "./utils/gpu-setup";
|
||||||
|
|
||||||
export * from "./lib/auth";
|
export * from "./lib/auth";
|
||||||
|
|
||||||
|
export {
|
||||||
|
startLogCleanup,
|
||||||
|
stopLogCleanup,
|
||||||
|
getLogCleanupStatus,
|
||||||
|
} from "./utils/access-log/handler";
|
||||||
|
|||||||
@@ -387,11 +387,13 @@ export const deployPreviewApplication = async ({
|
|||||||
titleLog = "Preview Deployment",
|
titleLog = "Preview Deployment",
|
||||||
descriptionLog = "",
|
descriptionLog = "",
|
||||||
previewDeploymentId,
|
previewDeploymentId,
|
||||||
|
isExternal = false,
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
|
isExternal?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
|
||||||
@@ -417,46 +419,43 @@ export const deployPreviewApplication = async ({
|
|||||||
githubId: application?.githubId || "",
|
githubId: application?.githubId || "",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const commentExists = await issueCommentExists({
|
if (!isExternal) {
|
||||||
...issueParams,
|
const commentExists = await issueCommentExists({
|
||||||
});
|
|
||||||
if (!commentExists) {
|
|
||||||
const result = await createPreviewDeploymentComment({
|
|
||||||
...issueParams,
|
...issueParams,
|
||||||
previewDomain,
|
|
||||||
appName: previewDeployment.appName,
|
|
||||||
githubId: application?.githubId || "",
|
|
||||||
previewDeploymentId,
|
|
||||||
});
|
});
|
||||||
|
if (!commentExists) {
|
||||||
if (!result) {
|
const result = await createPreviewDeploymentComment({
|
||||||
throw new TRPCError({
|
...issueParams,
|
||||||
code: "NOT_FOUND",
|
previewDomain,
|
||||||
message: "Pull request comment not found",
|
appName: previewDeployment.appName,
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
previewDeploymentId,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
if (!result) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Pull request comment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||||
|
}
|
||||||
|
const buildingComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"running",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const buildingComment = getIssueComment(
|
|
||||||
application.name,
|
|
||||||
"running",
|
|
||||||
previewDomain,
|
|
||||||
);
|
|
||||||
await updateIssueComment({
|
|
||||||
...issueParams,
|
|
||||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
|
||||||
});
|
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
|
|
||||||
// const admin = await findUserById(application.project.userId);
|
|
||||||
|
|
||||||
// if (admin.cleanupCacheOnPreviews) {
|
|
||||||
// await cleanupFullDocker(application?.serverId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
await cloneGithubRepository({
|
||||||
...application,
|
...application,
|
||||||
@@ -466,25 +465,31 @@ export const deployPreviewApplication = async ({
|
|||||||
});
|
});
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
}
|
}
|
||||||
const successComment = getIssueComment(
|
|
||||||
application.name,
|
if (!isExternal) {
|
||||||
"success",
|
const successComment = getIssueComment(
|
||||||
previewDomain,
|
application.name,
|
||||||
);
|
"success",
|
||||||
await updateIssueComment({
|
previewDomain,
|
||||||
...issueParams,
|
);
|
||||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
await updateIssueComment({
|
||||||
});
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "done",
|
previewStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
if (!isExternal) {
|
||||||
await updateIssueComment({
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
...issueParams,
|
await updateIssueComment({
|
||||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
...issueParams,
|
||||||
});
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "error",
|
previewStatus: "error",
|
||||||
@@ -500,11 +505,13 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
titleLog = "Preview Deployment",
|
titleLog = "Preview Deployment",
|
||||||
descriptionLog = "",
|
descriptionLog = "",
|
||||||
previewDeploymentId,
|
previewDeploymentId,
|
||||||
|
isExternal = false,
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
|
isExternal?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
|
||||||
@@ -530,36 +537,39 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
githubId: application?.githubId || "",
|
githubId: application?.githubId || "",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const commentExists = await issueCommentExists({
|
if (!isExternal) {
|
||||||
...issueParams,
|
const commentExists = await issueCommentExists({
|
||||||
});
|
|
||||||
if (!commentExists) {
|
|
||||||
const result = await createPreviewDeploymentComment({
|
|
||||||
...issueParams,
|
...issueParams,
|
||||||
previewDomain,
|
|
||||||
appName: previewDeployment.appName,
|
|
||||||
githubId: application?.githubId || "",
|
|
||||||
previewDeploymentId,
|
|
||||||
});
|
});
|
||||||
|
if (!commentExists) {
|
||||||
if (!result) {
|
const result = await createPreviewDeploymentComment({
|
||||||
throw new TRPCError({
|
...issueParams,
|
||||||
code: "NOT_FOUND",
|
previewDomain,
|
||||||
message: "Pull request comment not found",
|
appName: previewDeployment.appName,
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
previewDeploymentId,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
if (!result) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Pull request comment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||||
|
}
|
||||||
|
const buildingComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"running",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const buildingComment = getIssueComment(
|
|
||||||
application.name,
|
|
||||||
"running",
|
|
||||||
previewDomain,
|
|
||||||
);
|
|
||||||
await updateIssueComment({
|
|
||||||
...issueParams,
|
|
||||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
|
||||||
});
|
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
@@ -586,25 +596,29 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
await mechanizeDockerContainer(application);
|
await mechanizeDockerContainer(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
const successComment = getIssueComment(
|
if (!isExternal) {
|
||||||
application.name,
|
const successComment = getIssueComment(
|
||||||
"success",
|
application.name,
|
||||||
previewDomain,
|
"success",
|
||||||
);
|
previewDomain,
|
||||||
await updateIssueComment({
|
);
|
||||||
...issueParams,
|
await updateIssueComment({
|
||||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
...issueParams,
|
||||||
});
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "done",
|
previewStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
if (!isExternal) {
|
||||||
await updateIssueComment({
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
...issueParams,
|
await updateIssueComment({
|
||||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
...issueParams,
|
||||||
});
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "error",
|
previewStatus: "error",
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export const findPreviewDeploymentsByApplicationId = async (
|
|||||||
|
|
||||||
export const createPreviewDeployment = async (
|
export const createPreviewDeployment = async (
|
||||||
schema: typeof apiCreatePreviewDeployment._type,
|
schema: typeof apiCreatePreviewDeployment._type,
|
||||||
|
isExternal = false,
|
||||||
) => {
|
) => {
|
||||||
const application = await findApplicationById(schema.applicationId);
|
const application = await findApplicationById(schema.applicationId);
|
||||||
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||||
@@ -165,27 +166,32 @@ export const createPreviewDeployment = async (
|
|||||||
org?.ownerId || "",
|
org?.ownerId || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const octokit = authGithub(application?.github as Github);
|
let issueId = "";
|
||||||
|
if (!isExternal) {
|
||||||
|
const octokit = authGithub(application?.github as Github);
|
||||||
|
|
||||||
const runningComment = getIssueComment(
|
const runningComment = getIssueComment(
|
||||||
application.name,
|
application.name,
|
||||||
"initializing",
|
"initializing",
|
||||||
generateDomain,
|
generateDomain,
|
||||||
);
|
);
|
||||||
|
|
||||||
const issue = await octokit.rest.issues.createComment({
|
const issue = await octokit.rest.issues.createComment({
|
||||||
owner: application?.owner || "",
|
owner: application?.owner || "",
|
||||||
repo: application?.repository || "",
|
repo: application?.repository || "",
|
||||||
issue_number: Number.parseInt(schema.pullRequestNumber),
|
issue_number: Number.parseInt(schema.pullRequestNumber),
|
||||||
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
issueId = `${issue.data.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
const previewDeployment = await db
|
const previewDeployment = await db
|
||||||
.insert(previewDeployments)
|
.insert(previewDeployments)
|
||||||
.values({
|
.values({
|
||||||
...schema,
|
...schema,
|
||||||
appName: appName,
|
appName: appName,
|
||||||
pullRequestCommentId: `${issue.data.id}`,
|
pullRequestCommentId: issueId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -231,6 +237,13 @@ export const findPreviewDeploymentsByPullRequestId = async (
|
|||||||
) => {
|
) => {
|
||||||
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
||||||
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
||||||
|
with: {
|
||||||
|
application: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return previewDeploymentResult;
|
return previewDeploymentResult;
|
||||||
|
|||||||
@@ -1,121 +1,77 @@
|
|||||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import { type RotatingFileStream, createStream } from "rotating-file-stream";
|
|
||||||
import { execAsync } from "../process/execAsync";
|
import { execAsync } from "../process/execAsync";
|
||||||
import { findAdmin } from "@dokploy/server/services/admin";
|
import { findAdmin } from "@dokploy/server/services/admin";
|
||||||
import { updateUser } from "@dokploy/server/services/user";
|
import { updateUser } from "@dokploy/server/services/user";
|
||||||
|
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||||
|
|
||||||
class LogRotationManager {
|
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";
|
||||||
private static instance: LogRotationManager;
|
|
||||||
private stream: RotatingFileStream | null = null;
|
|
||||||
|
|
||||||
private constructor() {
|
export const startLogCleanup = async (
|
||||||
if (IS_CLOUD) {
|
cronExpression = "0 0 * * *",
|
||||||
return;
|
): Promise<boolean> => {
|
||||||
}
|
try {
|
||||||
this.initialize().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getInstance(): LogRotationManager {
|
|
||||||
if (!LogRotationManager.instance) {
|
|
||||||
LogRotationManager.instance = new LogRotationManager();
|
|
||||||
}
|
|
||||||
return LogRotationManager.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initialize(): Promise<void> {
|
|
||||||
const isActive = await this.getStateFromDB();
|
|
||||||
if (isActive) {
|
|
||||||
await this.activateStream();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getStateFromDB(): Promise<boolean> {
|
|
||||||
const admin = await findAdmin();
|
|
||||||
return admin?.user.enableLogRotation ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setStateInDB(active: boolean): Promise<void> {
|
|
||||||
const admin = await findAdmin();
|
|
||||||
if (!admin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await updateUser(admin.user.id, {
|
|
||||||
enableLogRotation: active,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async activateStream(): Promise<void> {
|
|
||||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||||
if (this.stream) {
|
|
||||||
await this.deactivateStream();
|
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
||||||
|
if (existingJob) {
|
||||||
|
existingJob.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stream = createStream("access.log", {
|
scheduleJob(LOG_CLEANUP_JOB_NAME, cronExpression, async () => {
|
||||||
size: "100M",
|
try {
|
||||||
interval: "1d",
|
await execAsync(
|
||||||
path: DYNAMIC_TRAEFIK_PATH,
|
`tail -n 1000 ${DYNAMIC_TRAEFIK_PATH}/access.log > ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp && mv ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp ${DYNAMIC_TRAEFIK_PATH}/access.log`,
|
||||||
rotate: 6,
|
);
|
||||||
compress: "gzip",
|
|
||||||
});
|
|
||||||
|
|
||||||
this.stream.on("rotation", this.handleRotation.bind(this));
|
await execAsync("docker exec dokploy-traefik kill -USR1 1");
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error("Error during log cleanup:", error);
|
||||||
private async deactivateStream(): Promise<void> {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
if (this.stream) {
|
|
||||||
this.stream.end(() => {
|
|
||||||
this.stream = null;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
public async activate(): Promise<boolean> {
|
const admin = await findAdmin();
|
||||||
const currentState = await this.getStateFromDB();
|
if (admin) {
|
||||||
if (currentState) {
|
await updateUser(admin.user.id, {
|
||||||
return true;
|
logCleanupCron: cronExpression,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setStateInDB(true);
|
|
||||||
await this.activateStream();
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public async deactivate(): Promise<boolean> {
|
export const stopLogCleanup = async (): Promise<boolean> => {
|
||||||
console.log("Deactivating log rotation...");
|
try {
|
||||||
const currentState = await this.getStateFromDB();
|
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
||||||
if (!currentState) {
|
if (existingJob) {
|
||||||
console.log("Log rotation is already inactive in DB");
|
existingJob.cancel();
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
const admin = await findAdmin();
|
||||||
|
if (admin) {
|
||||||
|
await updateUser(admin.user.id, {
|
||||||
|
logCleanupCron: null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setStateInDB(false);
|
|
||||||
await this.deactivateStream();
|
|
||||||
console.log("Log rotation deactivated successfully");
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error stopping log cleanup:", error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private async handleRotation() {
|
export const getLogCleanupStatus = async (): Promise<{
|
||||||
try {
|
enabled: boolean;
|
||||||
const status = await this.getStatus();
|
cronExpression: string | null;
|
||||||
if (!status) {
|
}> => {
|
||||||
await this.deactivateStream();
|
const admin = await findAdmin();
|
||||||
}
|
const cronExpression = admin?.user.logCleanupCron ?? null;
|
||||||
await execAsync(
|
return {
|
||||||
"docker kill -s USR1 $(docker ps -q --filter name=dokploy-traefik)",
|
enabled: cronExpression !== null,
|
||||||
);
|
cronExpression,
|
||||||
console.log("USR1 Signal send to Traefik");
|
};
|
||||||
} catch (error) {
|
};
|
||||||
console.error("Error sending USR1 Signal to Traefik:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public async getStatus(): Promise<boolean> {
|
|
||||||
const dbState = await this.getStateFromDB();
|
|
||||||
return dbState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const logRotationManager = LogRotationManager.getInstance();
|
|
||||||
|
|||||||
@@ -6,14 +6,21 @@ interface HourlyData {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processLogs(logString: string): HourlyData[] {
|
export function processLogs(
|
||||||
|
logString: string,
|
||||||
|
dateRange?: { start?: string; end?: string },
|
||||||
|
): HourlyData[] {
|
||||||
if (_.isEmpty(logString)) {
|
if (_.isEmpty(logString)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const hourlyData = _(logString)
|
const hourlyData = _(logString)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.compact()
|
.filter((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
// Check if the line starts with { and ends with } to ensure it's a potential JSON object
|
||||||
|
return trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}");
|
||||||
|
})
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
try {
|
try {
|
||||||
const log: LogEntry = JSON.parse(entry);
|
const log: LogEntry = JSON.parse(entry);
|
||||||
@@ -21,6 +28,20 @@ export function processLogs(logString: string): HourlyData[] {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const date = new Date(log.StartUTC);
|
const date = new Date(log.StartUTC);
|
||||||
|
|
||||||
|
if (dateRange?.start || dateRange?.end) {
|
||||||
|
const logDate = date.getTime();
|
||||||
|
const start = dateRange?.start
|
||||||
|
? new Date(dateRange.start).getTime()
|
||||||
|
: 0;
|
||||||
|
const end = dateRange?.end
|
||||||
|
? new Date(dateRange.end).getTime()
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
if (logDate < start || logDate > end) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return `${date.toISOString().slice(0, 13)}:00:00Z`;
|
return `${date.toISOString().slice(0, 13)}:00:00Z`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing log entry:", error);
|
console.error("Error parsing log entry:", error);
|
||||||
@@ -51,21 +72,46 @@ export function parseRawConfig(
|
|||||||
sort?: SortInfo,
|
sort?: SortInfo,
|
||||||
search?: string,
|
search?: string,
|
||||||
status?: string[],
|
status?: string[],
|
||||||
|
dateRange?: { start?: string; end?: string },
|
||||||
): { data: LogEntry[]; totalCount: number } {
|
): { data: LogEntry[]; totalCount: number } {
|
||||||
try {
|
try {
|
||||||
if (_.isEmpty(rawConfig)) {
|
if (_.isEmpty(rawConfig)) {
|
||||||
return { data: [], totalCount: 0 };
|
return { data: [], totalCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Split logs into chunks to avoid memory issues
|
||||||
let parsedLogs = _(rawConfig)
|
let parsedLogs = _(rawConfig)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
|
.filter((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
return (
|
||||||
|
trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((line) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(line) as LogEntry;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing log line:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
.compact()
|
.compact()
|
||||||
.map((line) => JSON.parse(line) as LogEntry)
|
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
parsedLogs = parsedLogs.filter(
|
// Apply date range filter if provided
|
||||||
(log) => log.ServiceName !== "dokploy-service-app@file",
|
if (dateRange?.start || dateRange?.end) {
|
||||||
);
|
parsedLogs = parsedLogs.filter((log) => {
|
||||||
|
const logDate = new Date(log.StartUTC).getTime();
|
||||||
|
const start = dateRange?.start
|
||||||
|
? new Date(dateRange.start).getTime()
|
||||||
|
: 0;
|
||||||
|
const end = dateRange?.end
|
||||||
|
? new Date(dateRange.end).getTime()
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
return logDate >= start && logDate <= end;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
parsedLogs = parsedLogs.filter((log) =>
|
parsedLogs = parsedLogs.filter((log) =>
|
||||||
@@ -78,6 +124,7 @@ export function parseRawConfig(
|
|||||||
status.some((range) => isStatusInRange(log.DownstreamStatus, range)),
|
status.some((range) => isStatusInRange(log.DownstreamStatus, range)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCount = parsedLogs.length;
|
const totalCount = parsedLogs.length;
|
||||||
|
|
||||||
if (sort) {
|
if (sort) {
|
||||||
@@ -101,6 +148,7 @@ export function parseRawConfig(
|
|||||||
throw new Error("Failed to parse rawConfig");
|
throw new Error("Failed to parse rawConfig");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStatusInRange = (status: number, range: string) => {
|
const isStatusInRange = (status: number, range: string) => {
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case "info":
|
case "info":
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function getProviderName(apiUrl: string) {
|
|||||||
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
|
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
|
||||||
return "ollama";
|
return "ollama";
|
||||||
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
||||||
throw new Error(`Unsupported AI provider for URL: ${apiUrl}`);
|
return "custom";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||||
@@ -67,7 +67,46 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
|||||||
baseURL: config.apiUrl,
|
baseURL: config.apiUrl,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
});
|
});
|
||||||
|
case "custom":
|
||||||
|
return createOpenAICompatible({
|
||||||
|
name: "custom",
|
||||||
|
baseURL: config.apiUrl,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported AI provider: ${providerName}`);
|
throw new Error(`Unsupported AI provider: ${providerName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getProviderHeaders = (
|
||||||
|
apiUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
): Record<string, string> => {
|
||||||
|
// Anthropic
|
||||||
|
if (apiUrl.includes("anthropic")) {
|
||||||
|
return {
|
||||||
|
"x-api-key": apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mistral
|
||||||
|
if (apiUrl.includes("mistral")) {
|
||||||
|
return {
|
||||||
|
Authorization: apiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default (OpenAI style)
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export interface Model {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created: number;
|
||||||
|
owned_by: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { runMongoBackup } from "./mongo";
|
|||||||
import { runMySqlBackup } from "./mysql";
|
import { runMySqlBackup } from "./mysql";
|
||||||
import { runPostgresBackup } from "./postgres";
|
import { runPostgresBackup } from "./postgres";
|
||||||
import { findAdmin } from "../../services/admin";
|
import { findAdmin } from "../../services/admin";
|
||||||
|
import { startLogCleanup } from "../access-log/handler";
|
||||||
|
|
||||||
export const initCronJobs = async () => {
|
export const initCronJobs = async () => {
|
||||||
console.log("Setting up cron jobs....");
|
console.log("Setting up cron jobs....");
|
||||||
@@ -168,4 +169,8 @@ export const initCronJobs = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (admin?.user.logCleanupCron) {
|
||||||
|
await startLogCleanup(admin.user.logCleanupCron);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
|||||||
if (groupName) {
|
if (groupName) {
|
||||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||||
}
|
}
|
||||||
return kind === "member";
|
return kind === "user";
|
||||||
});
|
});
|
||||||
const mappedRepositories = filteredRepos.map((repo: any) => {
|
const mappedRepositories = filteredRepos.map((repo: any) => {
|
||||||
return {
|
return {
|
||||||
@@ -433,7 +433,7 @@ export const testGitlabConnection = async (
|
|||||||
if (groupName) {
|
if (groupName) {
|
||||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||||
}
|
}
|
||||||
return kind === "member";
|
return kind === "user";
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredRepos.length;
|
return filteredRepos.length;
|
||||||
|
|||||||
@@ -137,12 +137,44 @@ export const readRemoteConfig = async (serverId: string, appName: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readMonitoringConfig = () => {
|
export const readMonitoringConfig = (readAll = false) => {
|
||||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
const yamlStr = fs.readFileSync(configPath, "utf8");
|
if (!readAll) {
|
||||||
return yamlStr;
|
// Read first 500 lines
|
||||||
|
let content = "";
|
||||||
|
let chunk = "";
|
||||||
|
let validCount = 0;
|
||||||
|
|
||||||
|
for (const char of fs.readFileSync(configPath, "utf8")) {
|
||||||
|
chunk += char;
|
||||||
|
if (char === "\n") {
|
||||||
|
try {
|
||||||
|
const trimmed = chunk.trim();
|
||||||
|
if (
|
||||||
|
trimmed !== "" &&
|
||||||
|
trimmed.startsWith("{") &&
|
||||||
|
trimmed.endsWith("}")
|
||||||
|
) {
|
||||||
|
const log = JSON.parse(trimmed);
|
||||||
|
if (log.ServiceName !== "dokploy-service-app@file") {
|
||||||
|
content += chunk;
|
||||||
|
validCount++;
|
||||||
|
if (validCount >= 500) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore invalid JSON
|
||||||
|
}
|
||||||
|
chunk = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return fs.readFileSync(configPath, "utf8");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
1
pnpm-lock.yaml
generated
1
pnpm-lock.yaml
generated
@@ -6282,6 +6282,7 @@ packages:
|
|||||||
|
|
||||||
oslo@1.2.0:
|
oslo@1.2.0:
|
||||||
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
|
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
|
||||||
|
deprecated: Package is no longer supported. Please see https://oslojs.dev for the successor project.
|
||||||
|
|
||||||
otpauth@9.3.4:
|
otpauth@9.3.4:
|
||||||
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
|
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
|
||||||
|
|||||||
Reference in New Issue
Block a user