mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-19 21:05:21 +02:00
Merge branch 'canary' into feat/ntfy
This commit is contained in:
@@ -27,6 +27,7 @@ if (typeof window === "undefined") {
|
||||
const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
herokuVersion: "",
|
||||
giteaBranch: "",
|
||||
giteaBuildPath: "",
|
||||
|
||||
74
apps/dokploy/__test__/env/shared.test.ts
vendored
74
apps/dokploy/__test__/env/shared.test.ts
vendored
@@ -177,3 +177,77 @@ COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareEnvironmentVariables (self references)", () => {
|
||||
it("resolves self references correctly", () => {
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=staging
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||
SELF_REF=\${{ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=staging",
|
||||
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
|
||||
"SELF_REF=staging",
|
||||
]);
|
||||
});
|
||||
|
||||
it("throws on undefined self references", () => {
|
||||
const serviceEnv = `
|
||||
MISSING_VAR=\${{UNDEFINED_VAR}}
|
||||
`;
|
||||
|
||||
expect(() => prepareEnvironmentVariables(serviceEnv, "")).toThrow(
|
||||
"Invalid service environment variable: UNDEFINED_VAR",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows overriding and still resolving from self", () => {
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=production
|
||||
OVERRIDE_ENV=\${{ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=production",
|
||||
"OVERRIDE_ENV=production",
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves multiple self references inside one value", () => {
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=staging
|
||||
APP_NAME=MyApp
|
||||
COMPLEX=\${{APP_NAME}}-\${{ENVIRONMENT}}-\${{APP_NAME}}
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=staging",
|
||||
"APP_NAME=MyApp",
|
||||
"COMPLEX=MyApp-staging-MyApp",
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles quotes with self references", () => {
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=production
|
||||
QUOTED="'\${{ENVIRONMENT}}'"
|
||||
MIXED="\"Double \${{ENVIRONMENT}}\""
|
||||
`;
|
||||
|
||||
const resolved = prepareEnvironmentVariables(serviceEnv, "");
|
||||
|
||||
expect(resolved).toEqual([
|
||||
"ENVIRONMENT=production",
|
||||
"QUOTED='production'",
|
||||
'MIXED="Double production"',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
rollbackActive: false,
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
herokuVersion: "",
|
||||
giteaRepository: "",
|
||||
giteaOwner: "",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -33,6 +34,12 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const schema = z
|
||||
@@ -42,6 +49,7 @@ const schema = z
|
||||
wildcardDomain: z.string(),
|
||||
port: z.number(),
|
||||
previewLimit: z.number(),
|
||||
previewLabels: z.array(z.string()).optional(),
|
||||
previewHttps: z.boolean(),
|
||||
previewPath: z.string(),
|
||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||
@@ -81,6 +89,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
wildcardDomain: "*.traefik.me",
|
||||
port: 3000,
|
||||
previewLimit: 3,
|
||||
previewLabels: [],
|
||||
previewHttps: false,
|
||||
previewPath: "/",
|
||||
previewCertificateType: "none",
|
||||
@@ -102,6 +111,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
buildArgs: data.previewBuildArgs || "",
|
||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||
port: data.previewPort || 3000,
|
||||
previewLabels: data.previewLabels || [],
|
||||
previewLimit: data.previewLimit || 3,
|
||||
previewHttps: data.previewHttps || false,
|
||||
previewPath: data.previewPath || "/",
|
||||
@@ -119,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
previewBuildArgs: formData.buildArgs,
|
||||
previewWildcard: formData.wildcardDomain,
|
||||
previewPort: formData.port,
|
||||
previewLabels: formData.previewLabels,
|
||||
applicationId,
|
||||
previewLimit: formData.previewLimit,
|
||||
previewHttps: formData.previewHttps,
|
||||
@@ -200,6 +211,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewLabels"
|
||||
render={({ field }) => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Preview Labels</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Add a labels that will trigger a preview
|
||||
deployment for a pull request. If no labels
|
||||
are specified, all pull requests will trigger
|
||||
a preview deployment.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{field.value?.map((label, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{label}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
onClick={() => {
|
||||
const newLabels = [...(field.value || [])];
|
||||
newLabels.splice(index, 1);
|
||||
field.onChange(newLabels);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter a label (e.g. enhancements, needs-review)"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const input = e.currentTarget;
|
||||
const label = input.value.trim();
|
||||
if (label) {
|
||||
field.onChange([
|
||||
...(field.value || []),
|
||||
label,
|
||||
]);
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const input = document.querySelector(
|
||||
'input[placeholder*="Enter a label"]',
|
||||
) as HTMLInputElement;
|
||||
const label = input.value.trim();
|
||||
if (label) {
|
||||
field.onChange([...(field.value || []), label]);
|
||||
input.value = "";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="previewLimit"
|
||||
|
||||
@@ -55,7 +55,12 @@ const formSchema = z
|
||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||
volumeName: z.string().min(1, "Volume name is required"),
|
||||
prefix: z.string(),
|
||||
// keepLatestCount: z.coerce.number().optional(),
|
||||
keepLatestCount: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.gte(1, "Must be at least 1")
|
||||
.optional()
|
||||
.nullable(),
|
||||
turnOff: z.boolean().default(false),
|
||||
enabled: z.boolean().default(true),
|
||||
serviceType: z.enum([
|
||||
@@ -108,6 +113,7 @@ export const HandleVolumeBackups = ({
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
|
||||
|
||||
const utils = api.useUtils();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -117,7 +123,7 @@ export const HandleVolumeBackups = ({
|
||||
cronExpression: "",
|
||||
volumeName: "",
|
||||
prefix: "",
|
||||
// keepLatestCount: undefined,
|
||||
keepLatestCount: undefined,
|
||||
turnOff: false,
|
||||
enabled: true,
|
||||
serviceName: "",
|
||||
@@ -173,13 +179,19 @@ export const HandleVolumeBackups = ({
|
||||
cronExpression: volumeBackup.cronExpression,
|
||||
volumeName: volumeBackup.volumeName || "",
|
||||
prefix: volumeBackup.prefix,
|
||||
// keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
||||
keepLatestCount: volumeBackup.keepLatestCount || undefined,
|
||||
turnOff: volumeBackup.turnOff,
|
||||
enabled: volumeBackup.enabled || false,
|
||||
serviceName: volumeBackup.serviceName || "",
|
||||
destinationId: volumeBackup.destinationId,
|
||||
serviceType: volumeBackup.serviceType,
|
||||
});
|
||||
setKeepLatestCountInput(
|
||||
volumeBackup.keepLatestCount !== null &&
|
||||
volumeBackup.keepLatestCount !== undefined
|
||||
? String(volumeBackup.keepLatestCount)
|
||||
: "",
|
||||
);
|
||||
}
|
||||
}, [form, volumeBackup, volumeBackupId]);
|
||||
|
||||
@@ -190,8 +202,12 @@ export const HandleVolumeBackups = ({
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
if (!id && !volumeBackupId) return;
|
||||
|
||||
const preparedKeepLatestCount =
|
||||
keepLatestCountInput === "" ? null : (values.keepLatestCount ?? null);
|
||||
|
||||
await mutateAsync({
|
||||
...values,
|
||||
keepLatestCount: preparedKeepLatestCount,
|
||||
destinationId: values.destinationId,
|
||||
volumeBackupId: volumeBackupId || "",
|
||||
serviceType: volumeBackupType,
|
||||
@@ -257,9 +273,8 @@ export const HandleVolumeBackups = ({
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"overflow-y-auto",
|
||||
volumeBackupType === "compose" || volumeBackupType === "application"
|
||||
? "max-h-[95vh] sm:max-w-2xl"
|
||||
? "sm:max-w-2xl"
|
||||
: " sm:max-w-lg",
|
||||
)}
|
||||
>
|
||||
@@ -600,29 +615,38 @@ export const HandleVolumeBackups = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="keepLatestCount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Keep Latest Count</FormLabel>
|
||||
<FormLabel>Keep Latest Backups</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="5"
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value) || undefined)
|
||||
}
|
||||
type="number"
|
||||
min={1}
|
||||
autoComplete="off"
|
||||
placeholder="Leave empty to keep all"
|
||||
value={keepLatestCountInput}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
setKeepLatestCountInput(raw);
|
||||
if (raw === "") {
|
||||
field.onChange(undefined);
|
||||
} else if (/^\d+$/.test(raw)) {
|
||||
field.onChange(Number(raw));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Number of backup files to keep (optional)
|
||||
How many recent backups to keep. Empty means no cleanup.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -114,6 +115,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled =
|
||||
(data &&
|
||||
"applicationStatus" in data &&
|
||||
data?.applicationStatus === "running") ||
|
||||
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -202,6 +209,12 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
{isDisabled && (
|
||||
<AlertBlock type="warning" className="w-full mt-5">
|
||||
Cannot delete the service while it is running. Please wait for the
|
||||
build to finish and then try again.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -211,8 +224,10 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={isDisabled}
|
||||
form="hook-form-delete-compose"
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const organizationSchema = z.object({
|
||||
@@ -54,6 +55,8 @@ export function AddOrganization({ organizationId }: Props) {
|
||||
const { mutateAsync, isLoading } = organizationId
|
||||
? api.organization.update.useMutation()
|
||||
: api.organization.create.useMutation();
|
||||
const { refetch: refetchActiveOrganization } =
|
||||
authClient.useActiveOrganization();
|
||||
|
||||
const form = useForm<OrganizationFormValues>({
|
||||
resolver: zodResolver(organizationSchema),
|
||||
@@ -84,6 +87,10 @@ export function AddOrganization({ organizationId }: Props) {
|
||||
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
||||
);
|
||||
utils.organization.all.invalidate();
|
||||
if (organizationId) {
|
||||
utils.organization.one.invalidate({ organizationId });
|
||||
refetchActiveOrganization();
|
||||
}
|
||||
setOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -38,7 +38,7 @@ import { api } from "@/utils/api";
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
||||
apiKey: z.string().min(1, { message: "API Key is required" }),
|
||||
apiKey: z.string(),
|
||||
model: z.string().min(1, { message: "Model is required" }),
|
||||
isEnabled: z.boolean(),
|
||||
});
|
||||
@@ -71,7 +71,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
name: "",
|
||||
apiUrl: "",
|
||||
apiKey: "",
|
||||
model: "gpt-3.5-turbo",
|
||||
model: "",
|
||||
isEnabled: true,
|
||||
},
|
||||
});
|
||||
@@ -81,7 +81,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
name: data?.name ?? "",
|
||||
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
||||
apiKey: data?.apiKey ?? "",
|
||||
model: data?.model ?? "gpt-3.5-turbo",
|
||||
model: data?.model ?? "",
|
||||
isEnabled: data?.isEnabled ?? true,
|
||||
});
|
||||
}, [aiId, form, data]);
|
||||
@@ -89,6 +89,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
const apiUrl = form.watch("apiUrl");
|
||||
const apiKey = form.watch("apiKey");
|
||||
|
||||
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
|
||||
const { data: models, isLoading: isLoadingServerModels } =
|
||||
api.ai.getModels.useQuery(
|
||||
{
|
||||
@@ -96,7 +97,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
apiKey: apiKey ?? "",
|
||||
},
|
||||
{
|
||||
enabled: !!apiUrl && !!apiKey,
|
||||
enabled: !!apiUrl && (isOllama || !!apiKey),
|
||||
onError: (error) => {
|
||||
setError(`Failed to fetch models: ${error.message}`);
|
||||
},
|
||||
@@ -191,22 +192,29 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="sk-..." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your API key for authentication
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!isOllama && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Key</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
autoComplete="one-time-code"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your API key for authentication
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoadingServerModels && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
@@ -214,6 +222,12 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!isLoadingServerModels && !models?.length && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No models available
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!isLoadingServerModels && models && models.length > 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -11,11 +11,20 @@ interface Props {
|
||||
export const DashboardLayout = ({ children }: Props) => {
|
||||
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: isUserSubscribed } = api.settings.isUserSubscribed.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: isCloud === true,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
{isCloud === true && (
|
||||
{isCloud === true && isUserSubscribed === true && (
|
||||
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
||||
)}
|
||||
|
||||
|
||||
@@ -129,10 +129,11 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-50 pointer-events-auto w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"flex flex-col max-h-[90vh]",
|
||||
className,
|
||||
)}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
onInteractOutside={(event) => event.preventDefault()}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -148,7 +148,7 @@ const SidebarProvider = React.forwardRef<
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
"group/sidebar-wrapper flex h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -329,10 +329,11 @@ const SidebarInset = React.forwardRef<
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
|
||||
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
|
||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className,
|
||||
)}
|
||||
style={{ scrollbarGutter: "stable" }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
1
apps/dokploy/drizzle/0106_purple_maggott.sql
Normal file
1
apps/dokploy/drizzle/0106_purple_maggott.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "previewLabels" text[];
|
||||
6424
apps/dokploy/drizzle/meta/0106_snapshot.json
Normal file
6424
apps/dokploy/drizzle/meta/0106_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -743,6 +743,13 @@
|
||||
"when": 1754259281559,
|
||||
"tag": "0105_clumsy_quicksilver",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 106,
|
||||
"version": "7",
|
||||
"when": 1754912062243,
|
||||
"tag": "0106_purple_maggott",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.24.11",
|
||||
"version": "v0.25.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -37,13 +37,13 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/azure": "^1.3.23",
|
||||
"@ai-sdk/cohere": "^1.2.10",
|
||||
"@ai-sdk/deepinfra": "^0.0.4",
|
||||
"@ai-sdk/mistral": "^1.2.8",
|
||||
"@ai-sdk/openai": "^1.3.22",
|
||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
||||
"@ai-sdk/anthropic": "^2.0.5",
|
||||
"@ai-sdk/azure": "^2.0.16",
|
||||
"@ai-sdk/cohere": "^2.0.4",
|
||||
"@ai-sdk/deepinfra": "^1.0.10",
|
||||
"@ai-sdk/mistral": "^2.0.7",
|
||||
"@ai-sdk/openai": "^2.0.16",
|
||||
"@ai-sdk/openai-compatible": "^1.0.10",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
@@ -91,7 +91,8 @@
|
||||
"@xterm/addon-clipboard": "0.1.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"ai": "^4.3.16",
|
||||
"ai": "^5.0.17",
|
||||
"ai-sdk-ollama": "^0.5.1",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "v1.2.8-beta.7",
|
||||
"bl": "6.0.11",
|
||||
@@ -124,7 +125,6 @@
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
"ollama-ai-provider": "^1.2.0",
|
||||
"otpauth": "^9.4.0",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
|
||||
@@ -343,7 +343,9 @@ export default async function handler(
|
||||
if (
|
||||
action === "opened" ||
|
||||
action === "synchronize" ||
|
||||
action === "reopened"
|
||||
action === "reopened" ||
|
||||
action === "labeled" ||
|
||||
action === "unlabeled"
|
||||
) {
|
||||
const repository = githubBody?.repository?.name;
|
||||
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||
@@ -442,6 +444,19 @@ export default async function handler(
|
||||
}
|
||||
|
||||
for (const app of secureApps) {
|
||||
// check for labels
|
||||
if (app?.previewLabels && app?.previewLabels?.length > 0) {
|
||||
let hasLabel = false;
|
||||
const labels = githubBody?.pull_request?.labels;
|
||||
for (const label of labels) {
|
||||
if (app?.previewLabels?.includes(label.name)) {
|
||||
hasLabel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasLabel) continue;
|
||||
}
|
||||
|
||||
const previewLimit = app?.previewLimit || 0;
|
||||
if (app?.previewDeployments?.length > previewLimit) {
|
||||
continue;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
FolderInput,
|
||||
GlobeIcon,
|
||||
Loader2,
|
||||
Play,
|
||||
PlusIcon,
|
||||
Search,
|
||||
ServerIcon,
|
||||
@@ -40,10 +41,12 @@ import {
|
||||
RedisIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -288,6 +291,8 @@ const Project = (
|
||||
const [openCombobox, setOpenCombobox] = useState(false);
|
||||
const [selectedServices, setSelectedServices] = useState<string[]>([]);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedServices.length === filteredServices.length) {
|
||||
@@ -311,6 +316,7 @@ const Project = (
|
||||
stop: api.compose.stop.useMutation(),
|
||||
move: api.compose.move.useMutation(),
|
||||
delete: api.compose.delete.useMutation(),
|
||||
deploy: api.compose.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const applicationActions = {
|
||||
@@ -318,6 +324,7 @@ const Project = (
|
||||
stop: api.application.stop.useMutation(),
|
||||
move: api.application.move.useMutation(),
|
||||
delete: api.application.delete.useMutation(),
|
||||
deploy: api.application.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const postgresActions = {
|
||||
@@ -325,6 +332,7 @@ const Project = (
|
||||
stop: api.postgres.stop.useMutation(),
|
||||
move: api.postgres.move.useMutation(),
|
||||
delete: api.postgres.remove.useMutation(),
|
||||
deploy: api.postgres.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const mysqlActions = {
|
||||
@@ -332,6 +340,7 @@ const Project = (
|
||||
stop: api.mysql.stop.useMutation(),
|
||||
move: api.mysql.move.useMutation(),
|
||||
delete: api.mysql.remove.useMutation(),
|
||||
deploy: api.mysql.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const mariadbActions = {
|
||||
@@ -339,6 +348,7 @@ const Project = (
|
||||
stop: api.mariadb.stop.useMutation(),
|
||||
move: api.mariadb.move.useMutation(),
|
||||
delete: api.mariadb.remove.useMutation(),
|
||||
deploy: api.mariadb.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const redisActions = {
|
||||
@@ -346,6 +356,7 @@ const Project = (
|
||||
stop: api.redis.stop.useMutation(),
|
||||
move: api.redis.move.useMutation(),
|
||||
delete: api.redis.remove.useMutation(),
|
||||
deploy: api.redis.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const mongoActions = {
|
||||
@@ -353,6 +364,7 @@ const Project = (
|
||||
stop: api.mongo.stop.useMutation(),
|
||||
move: api.mongo.move.useMutation(),
|
||||
delete: api.mongo.remove.useMutation(),
|
||||
deploy: api.mongo.deploy.useMutation(),
|
||||
};
|
||||
|
||||
const handleBulkStart = async () => {
|
||||
@@ -523,7 +535,7 @@ const Project = (
|
||||
setIsBulkActionLoading(false);
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
const handleBulkDelete = async (deleteVolumes = false) => {
|
||||
let success = 0;
|
||||
setIsBulkActionLoading(true);
|
||||
for (const serviceId of selectedServices) {
|
||||
@@ -540,7 +552,7 @@ const Project = (
|
||||
case "compose":
|
||||
await composeActions.delete.mutateAsync({
|
||||
composeId: serviceId,
|
||||
deleteVolumes: false,
|
||||
deleteVolumes,
|
||||
});
|
||||
break;
|
||||
case "postgres":
|
||||
@@ -585,6 +597,83 @@ const Project = (
|
||||
setIsBulkActionLoading(false);
|
||||
};
|
||||
|
||||
const handleBulkDeploy = async () => {
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
setIsBulkActionLoading(true);
|
||||
|
||||
for (const serviceId of selectedServices) {
|
||||
try {
|
||||
const service = filteredServices.find((s) => s.id === serviceId);
|
||||
if (!service) continue;
|
||||
|
||||
switch (service.type) {
|
||||
case "application":
|
||||
await applicationActions.deploy.mutateAsync({
|
||||
applicationId: serviceId,
|
||||
});
|
||||
break;
|
||||
case "compose":
|
||||
await composeActions.deploy.mutateAsync({
|
||||
composeId: serviceId,
|
||||
});
|
||||
|
||||
break;
|
||||
case "postgres":
|
||||
await postgresActions.deploy.mutateAsync({
|
||||
postgresId: serviceId,
|
||||
});
|
||||
|
||||
break;
|
||||
case "mysql":
|
||||
await mysqlActions.deploy.mutateAsync({
|
||||
mysqlId: serviceId,
|
||||
});
|
||||
|
||||
break;
|
||||
case "mariadb":
|
||||
await mariadbActions.deploy.mutateAsync({
|
||||
mariadbId: serviceId,
|
||||
});
|
||||
|
||||
break;
|
||||
case "redis":
|
||||
await redisActions.deploy.mutateAsync({
|
||||
redisId: serviceId,
|
||||
});
|
||||
|
||||
break;
|
||||
case "mongo":
|
||||
await mongoActions.deploy.mutateAsync({
|
||||
mongoId: serviceId,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
success++;
|
||||
} catch (error) {
|
||||
failed++;
|
||||
toast.error(
|
||||
`Error deploying service ${serviceId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (success > 0) {
|
||||
toast.success(
|
||||
`${success} service${success !== 1 ? "s" : ""} deployed successfully`,
|
||||
);
|
||||
}
|
||||
if (failed > 0) {
|
||||
toast.error(
|
||||
`${failed} service${failed !== 1 ? "s" : ""} failed to deploy`,
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedServices([]);
|
||||
setIsDropdownOpen(false);
|
||||
setIsBulkActionLoading(false);
|
||||
};
|
||||
|
||||
const filteredServices = useMemo(() => {
|
||||
if (!applications) return [];
|
||||
const filtered = applications.filter(
|
||||
@@ -598,6 +687,13 @@ const Project = (
|
||||
return sortServices(filtered);
|
||||
}, [applications, searchQuery, selectedTypes, sortBy]);
|
||||
|
||||
const selectedServicesWithRunningStatus = useMemo(() => {
|
||||
return filteredServices.filter(
|
||||
(service) =>
|
||||
selectedServices.includes(service.id) && service.status === "running",
|
||||
);
|
||||
}, [filteredServices, selectedServices]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BreadcrumbSidebar
|
||||
@@ -721,6 +817,24 @@ const Project = (
|
||||
Start
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Deploy Services"
|
||||
description={`Are you sure you want to deploy ${selectedServices.length} service${selectedServices.length !== 1 ? "s" : ""}? This will redeploy/restart the selected services.`}
|
||||
onClick={handleBulkDeploy}
|
||||
type="default"
|
||||
disabled={
|
||||
selectedServices.length === 0 ||
|
||||
isBulkActionLoading
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start"
|
||||
>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Deploy
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DialogAction
|
||||
title="Stop Services"
|
||||
description={`Are you sure you want to stop ${selectedServices.length} services?`}
|
||||
@@ -740,9 +854,35 @@ const Project = (
|
||||
<>
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
Are you sure you want to delete{" "}
|
||||
{selectedServices.length} services? This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
{selectedServicesWithRunningStatus.length >
|
||||
0 && (
|
||||
<AlertBlock type="warning">
|
||||
Warning:{" "}
|
||||
{
|
||||
selectedServicesWithRunningStatus.length
|
||||
}{" "}
|
||||
of the selected services are currently
|
||||
running. Please stop these services
|
||||
first before deleting:{" "}
|
||||
{selectedServicesWithRunningStatus
|
||||
.map((s) => s.name)
|
||||
.join(", ")}
|
||||
</AlertBlock>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
type="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
disabled={
|
||||
selectedServicesWithRunningStatus.length > 0
|
||||
}
|
||||
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -838,6 +978,113 @@ const Project = (
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Bulk Delete Dialog */}
|
||||
<Dialog
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Services</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete{" "}
|
||||
{selectedServices.length} service
|
||||
{selectedServices.length !== 1 ? "s" : ""}?
|
||||
This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Show services to be deleted */}
|
||||
<div className="max-h-40 overflow-y-auto space-y-2">
|
||||
{selectedServices.map((serviceId) => {
|
||||
const service = filteredServices.find(
|
||||
(s) => s.id === serviceId,
|
||||
);
|
||||
return service ? (
|
||||
<div
|
||||
key={serviceId}
|
||||
className="flex items-center space-x-2 text-sm"
|
||||
>
|
||||
<span className="px-2 py-1 text-xs bg-secondary rounded">
|
||||
{service.type}
|
||||
</span>
|
||||
<span>{service.name}</span>
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Volume deletion option for compose services */}
|
||||
{(() => {
|
||||
const servicesWithVolumeSupport =
|
||||
selectedServices.filter((serviceId) => {
|
||||
const service = filteredServices.find(
|
||||
(s) => s.id === serviceId,
|
||||
);
|
||||
// Currently only compose services support volume deletion
|
||||
return service?.type === "compose";
|
||||
});
|
||||
|
||||
if (servicesWithVolumeSupport.length === 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="deleteVolumes"
|
||||
checked={deleteVolumes}
|
||||
onCheckedChange={(checked) =>
|
||||
setDeleteVolumes(checked === true)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="deleteVolumes"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Delete volumes associated with
|
||||
services
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Volume deletion is available for:{" "}
|
||||
{servicesWithVolumeSupport.length}{" "}
|
||||
compose service
|
||||
{servicesWithVolumeSupport.length !== 1
|
||||
? "s"
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setDeleteVolumes(false); // Reset checkbox
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
handleBulkDelete(deleteVolumes);
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setDeleteVolumes(false); // Reset checkbox
|
||||
}}
|
||||
disabled={isBulkActionLoading}
|
||||
>
|
||||
Delete Services
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -329,6 +329,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
||||
maxLength={6}
|
||||
pattern={REGEXP_ONLY_DIGITS}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} className="border-border" />
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@dokploy/server/services/user";
|
||||
import {
|
||||
getProviderHeaders,
|
||||
getProviderName,
|
||||
type Model,
|
||||
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -47,11 +48,24 @@ export const aiRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
getModels: protectedProcedure
|
||||
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string().min(1) }))
|
||||
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
const providerName = getProviderName(input.apiUrl);
|
||||
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
||||
const response = await fetch(`${input.apiUrl}/models`, { headers });
|
||||
let response = null;
|
||||
switch (providerName) {
|
||||
case "ollama":
|
||||
response = await fetch(`${input.apiUrl}/api/tags`, { headers });
|
||||
break;
|
||||
default:
|
||||
if (!input.apiKey)
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "API key must contain at least 1 character(s)",
|
||||
});
|
||||
response = await fetch(`${input.apiUrl}/models`, { headers });
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
@@ -80,7 +80,7 @@ export const redisRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
return true;
|
||||
return newRedis;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { z } from "zod";
|
||||
@@ -60,6 +60,8 @@ import {
|
||||
apiServerSchema,
|
||||
apiTraefikConfig,
|
||||
apiUpdateDockerCleanup,
|
||||
projects,
|
||||
server,
|
||||
} from "@/server/db/schema";
|
||||
import { removeJob, schedule } from "@/server/utils/backup";
|
||||
import packageInfo from "../../../package.json";
|
||||
@@ -706,6 +708,18 @@ export const settingsRouter = createTRPCRouter({
|
||||
isCloud: publicProcedure.query(async () => {
|
||||
return IS_CLOUD;
|
||||
}),
|
||||
isUserSubscribed: protectedProcedure.query(async ({ ctx }) => {
|
||||
const haveServers = await db.query.server.findMany({
|
||||
where: eq(server.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
});
|
||||
const haveProjects = await db.query.projects.findMany({
|
||||
where: eq(
|
||||
projects.organizationId,
|
||||
ctx.session?.activeOrganizationId || "",
|
||||
),
|
||||
});
|
||||
return haveServers.length > 0 || haveProjects.length > 0;
|
||||
}),
|
||||
health: publicProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
try {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
await initializeNetwork();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
await execAsync("docker pull traefik:v3.1.2");
|
||||
await execAsync("docker pull traefik:v3.5.0");
|
||||
await initializeStandaloneTraefik();
|
||||
await initializeRedis();
|
||||
await initializePostgres();
|
||||
|
||||
Reference in New Issue
Block a user