Merge branch 'canary' into Move-environment-variables-icon-outside-dropdown-#2755

This commit is contained in:
Mauricio Siu
2025-10-25 00:54:00 -06:00
24 changed files with 245 additions and 53 deletions
+4
View File
@@ -77,6 +77,10 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<div> <div>
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a> <a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a> <a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
<a href="https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor" target="_blank">
<img src="https://www.lambdatest.com/blue-logo.png" width="450" height="100" />
</a>
</div> </div>
<!-- Premium Supporters 🥇 --> <!-- Premium Supporters 🥇 -->
@@ -228,5 +228,58 @@ describe("helpers functions", () => {
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
); );
}); });
it("should handle JWT payload with newlines and whitespace by trimming them", () => {
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
const expiry = iat + 3600;
const payloadWithNewlines = `{
"role": "anon",
"iss": "supabase",
"exp": ${expiry}
}
`;
const jwt = processValue(
"${jwt:secret:payload}",
{
secret: "mysecret",
payload: payloadWithNewlines,
},
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
jwtCheckHeader(parts[0]);
const decodedPayload = jwtBase64Decode(parts[1]);
expect(decodedPayload).toHaveProperty("role");
expect(decodedPayload.role).toEqual("anon");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload.iss).toEqual("supabase");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.exp).toEqual(expiry);
});
it("should handle JWT payload with leading and trailing whitespace", () => {
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
const expiry = iat + 3600;
const payloadWithWhitespace = ` {"role": "service_role", "iss": "supabase", "exp": ${expiry}} `;
const jwt = processValue(
"${jwt:secret:payload}",
{
secret: "mysecret",
payload: payloadWithWhitespace,
},
mockSchema,
);
expect(jwt).toMatch(jwtMatchExp);
const parts = jwt.split(".") as JWTParts;
jwtCheckHeader(parts[0]);
const decodedPayload = jwtBase64Decode(parts[1]);
expect(decodedPayload).toHaveProperty("role");
expect(decodedPayload.role).toEqual("service_role");
expect(decodedPayload).toHaveProperty("iss");
expect(decodedPayload.iss).toEqual("supabase");
expect(decodedPayload).toHaveProperty("exp");
expect(decodedPayload.exp).toEqual(expiry);
});
}); });
}); });
@@ -150,7 +150,10 @@ export const ShowResources = ({ id, type }: Props) => {
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<div className="flex items-center gap-2"> <div
className="flex items-center gap-2"
onClick={(e) => e.preventDefault()}
>
<FormLabel>Memory Limit</FormLabel> <FormLabel>Memory Limit</FormLabel>
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={0}> <Tooltip delayDuration={0}>
@@ -182,7 +185,10 @@ export const ShowResources = ({ id, type }: Props) => {
name="memoryReservation" name="memoryReservation"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<div className="flex items-center gap-2"> <div
className="flex items-center gap-2"
onClick={(e) => e.preventDefault()}
>
<FormLabel>Memory Reservation</FormLabel> <FormLabel>Memory Reservation</FormLabel>
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={0}> <Tooltip delayDuration={0}>
@@ -215,7 +221,10 @@ export const ShowResources = ({ id, type }: Props) => {
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<div className="flex items-center gap-2"> <div
className="flex items-center gap-2"
onClick={(e) => e.preventDefault()}
>
<FormLabel>CPU Limit</FormLabel> <FormLabel>CPU Limit</FormLabel>
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={0}> <Tooltip delayDuration={0}>
@@ -249,7 +258,10 @@ export const ShowResources = ({ id, type }: Props) => {
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem>
<div className="flex items-center gap-2"> <div
className="flex items-center gap-2"
onClick={(e) => e.preventDefault()}
>
<FormLabel>CPU Reservation</FormLabel> <FormLabel>CPU Reservation</FormLabel>
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={0}> <Tooltip delayDuration={0}>
@@ -150,7 +150,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false, enableSubmodules: data.enableSubmodules || false,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provider Saved");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
@@ -149,7 +149,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules, enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provider Saved");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
@@ -167,7 +167,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules, enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provider Saved");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
@@ -48,10 +48,8 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
); );
const utils = api.useUtils(); const utils = api.useUtils();
const { mutateAsync: deleteSchedule, isLoading: isDeleting } = const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
api.schedule.delete.useMutation(); api.schedule.delete.useMutation();
const { mutateAsync: runManually, isLoading } = const { mutateAsync: runManually, isLoading } =
api.schedule.runManually.useMutation(); api.schedule.runManually.useMutation();
@@ -67,7 +65,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
Schedule tasks to run automatically at specified intervals. Schedule tasks to run automatically at specified intervals.
</CardDescription> </CardDescription>
</div> </div>
{schedules && schedules.length > 0 && ( {schedules && schedules.length > 0 && (
<HandleSchedules id={id} scheduleType={scheduleType} /> <HandleSchedules id={id} scheduleType={scheduleType} />
)} )}
@@ -75,7 +72,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
</CardHeader> </CardHeader>
<CardContent className="px-0"> <CardContent className="px-0">
{isLoadingSchedules ? ( {isLoadingSchedules ? (
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]"> <div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" /> <Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
<span className="text-sm text-muted-foreground/70"> <span className="text-sm text-muted-foreground/70">
Loading scheduled tasks... Loading scheduled tasks...
@@ -91,13 +88,13 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
return ( return (
<div <div
key={schedule.scheduleId} key={schedule.scheduleId}
className="flex items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50" className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3 w-full sm:w-auto">
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5"> <div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
<Clock className="size-4 text-primary/70" /> <Clock className="size-4 text-primary/70" />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5 w-full sm:w-auto">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3"> <h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
{schedule.name} {schedule.name}
@@ -132,27 +129,25 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
)} )}
</div> </div>
{schedule.command && ( {schedule.command && (
<div className="flex items-center gap-2"> <div className="flex items-start gap-2 max-w-full">
<Terminal className="size-3.5 text-muted-foreground/70" /> <Terminal className="size-3.5 text-muted-foreground/70 flex-shrink-0 mt-0.5" />
<code className="font-mono text-[10px] text-muted-foreground/70"> <code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
{schedule.command} {schedule.command}
</code> </code>
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-0.5 md:gap-1.5 mt-2 sm:mt-0 sm:ml-3">
<div className="flex items-center gap-0.5 md:gap-1.5">
<ShowDeploymentsModal <ShowDeploymentsModal
id={schedule.scheduleId} id={schedule.scheduleId}
type="schedule" type="schedule"
serverId={serverId || undefined} serverId={serverId || undefined}
> >
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<ClipboardList className="size-4 transition-colors " /> <ClipboardList className="size-4 transition-colors" />
</Button> </Button>
</ShowDeploymentsModal> </ShowDeploymentsModal>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -163,7 +158,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
isLoading={isLoading} isLoading={isLoading}
onClick={async () => { onClick={async () => {
toast.success("Schedule run successfully"); toast.success("Schedule run successfully");
await runManually({ await runManually({
scheduleId: schedule.scheduleId, scheduleId: schedule.scheduleId,
}) })
@@ -178,19 +172,17 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
}); });
}} }}
> >
<Play className="size-4 transition-colors" /> <Play className="size-4 transition-colors" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Run Manual Schedule</TooltipContent> <TooltipContent>Run Manual Schedule</TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<HandleSchedules <HandleSchedules
scheduleId={schedule.scheduleId} scheduleId={schedule.scheduleId}
id={id} id={id}
scheduleType={scheduleType} scheduleType={scheduleType}
/> />
<DialogAction <DialogAction
title="Delete Schedule" title="Delete Schedule"
description="Are you sure you want to delete this schedule?" description="Are you sure you want to delete this schedule?"
@@ -214,7 +206,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="group hover:bg-red-500/10 " className="group hover:bg-red-500/10"
isLoading={isDeleting} isLoading={isDeleting}
> >
<Trash2 className="size-4 text-primary group-hover:text-red-500" /> <Trash2 className="size-4 text-primary group-hover:text-red-500" />
@@ -104,7 +104,7 @@ export const DeleteService = ({ id, type }: Props) => {
push( push(
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`, `/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
); );
toast.success("deleted successfully"); toast.success("Service deleted successfully");
setIsOpen(false); setIsOpen(false);
}) })
.catch(() => { .catch(() => {
@@ -152,7 +152,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules, enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provider Saved");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
@@ -151,7 +151,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
triggerType: data.triggerType, triggerType: data.triggerType,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provider Saved");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
@@ -160,7 +160,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules, enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provider Saved");
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
@@ -281,6 +281,7 @@ export const ImpersonationBar = () => {
<div className="flex items-center gap-4 flex-1 flex-wrap"> <div className="flex items-center gap-4 flex-1 flex-wrap">
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarImage <AvatarImage
className="object-cover"
src={data?.user?.image || ""} src={data?.user?.image || ""}
alt={data?.user?.name || ""} alt={data?.user?.name || ""}
/> />
@@ -55,7 +55,7 @@ import { api } from "@/utils/api";
type DbType = typeof mySchema._type.type; type DbType = typeof mySchema._type.type;
const dockerImageDefaultPlaceholder: Record<DbType, string> = { const dockerImageDefaultPlaceholder: Record<DbType, string> = {
mongo: "mongo:6", mongo: "mongo:7",
mariadb: "mariadb:11", mariadb: "mariadb:11",
mysql: "mysql:8", mysql: "mysql:8",
postgres: "postgres:15", postgres: "postgres:15",
@@ -217,7 +217,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{(isError || isErrorConnection) && ( {(isError || isErrorConnection) && (
<AlertBlock type="error" className="break-words"> <AlertBlock type="error" className="w-full">
{connectionError?.message || error?.message} {connectionError?.message || error?.message}
</AlertBlock> </AlertBlock>
)} )}
@@ -208,10 +208,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
}); });
useEffect(() => { useEffect(() => {
if (type === "email") { if (type === "email" && fields.length === 0) {
append(""); append("");
} }
}, [type, append]); }, [type, append, fields.length]);
useEffect(() => { useEffect(() => {
if (notification) { if (notification) {
@@ -1,5 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Fingerprint, QrCode } from "lucide-react"; import copy from "copy-to-clipboard";
import { CopyIcon, DownloadIcon, Fingerprint, QrCode } from "lucide-react";
import QRCode from "qrcode"; import QRCode from "qrcode";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -29,6 +30,12 @@ import {
InputOTPGroup, InputOTPGroup,
InputOTPSlot, InputOTPSlot,
} from "@/components/ui/input-otp"; } from "@/components/ui/input-otp";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -54,6 +61,26 @@ type TwoFactorSetupData = {
type PasswordForm = z.infer<typeof PasswordSchema>; type PasswordForm = z.infer<typeof PasswordSchema>;
type PinForm = z.infer<typeof PinSchema>; type PinForm = z.infer<typeof PinSchema>;
const USERNAME_PLACEHOLDER = "%username%";
const DATE_PLACEHOLDER = "%date%";
const BACKUP_CODES_PLACEHOLDER = "%backupCodes%";
const backupCodeTemplate = `Dokploy - BACKUP VERIFICATION CODES
Points to note
--------------
# Each code can be used only once.
# Do not share these codes with anyone.
Generated codes
---------------
Username: ${USERNAME_PLACEHOLDER}
Generated on: ${DATE_PLACEHOLDER}
${BACKUP_CODES_PLACEHOLDER}
`;
export const Enable2FA = () => { export const Enable2FA = () => {
const utils = api.useUtils(); const utils = api.useUtils();
const [data, setData] = useState<TwoFactorSetupData | null>(null); const [data, setData] = useState<TwoFactorSetupData | null>(null);
@@ -62,6 +89,7 @@ export const Enable2FA = () => {
const [step, setStep] = useState<"password" | "verify">("password"); const [step, setStep] = useState<"password" | "verify">("password");
const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [otpValue, setOtpValue] = useState(""); const [otpValue, setOtpValue] = useState("");
const { data: currentUser } = api.user.get.useQuery();
const handleVerifySubmit = async (e: React.FormEvent) => { const handleVerifySubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -178,6 +206,54 @@ export const Enable2FA = () => {
} }
}; };
const handleDownloadBackupCodes = () => {
if (!backupCodes || backupCodes.length === 0) {
toast.error("No backup codes to download.");
return;
}
const backupCodesFormatted = backupCodes
.map((code, index) => ` ${index + 1}. ${code}`)
.join("\n");
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const filename = `dokploy-2fa-backup-codes-${year}${month}${day}.txt`;
const backupCodesText = backupCodeTemplate
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
.replace(DATE_PLACEHOLDER, date.toLocaleString())
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
const blob = new Blob([backupCodesText], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleCopyBackupCodes = () => {
const date = new Date();
const backupCodesFormatted = backupCodes
.map((code, index) => ` ${index + 1}. ${code}`)
.join("\n");
const backupCodesText = backupCodeTemplate
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
.replace(DATE_PLACEHOLDER, date.toLocaleString())
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
copy(backupCodesText);
toast.success("Backup codes copied to clipboard");
};
return ( return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -264,6 +340,7 @@ export const Enable2FA = () => {
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Scan this QR code with your authenticator app Scan this QR code with your authenticator app
</span> </span>
{/** biome-ignore lint/performance/noImgElement: This is a valid use case for an img element */}
<img <img
src={data.qrCodeUrl} src={data.qrCodeUrl}
alt="2FA QR Code" alt="2FA QR Code"
@@ -281,7 +358,46 @@ export const Enable2FA = () => {
{backupCodes && backupCodes.length > 0 && ( {backupCodes && backupCodes.length > 0 && (
<div className="w-full space-y-3 border rounded-lg p-4"> <div className="w-full space-y-3 border rounded-lg p-4">
<h4 className="font-medium">Backup Codes</h4> <div className="flex items-center justify-between">
<h4 className="font-medium">Backup Codes</h4>
<div className="flex items-center gap-2">
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleCopyBackupCodes}
>
<CopyIcon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copy</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleDownloadBackupCodes}
>
<DownloadIcon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Download</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{backupCodes.map((code, index) => ( {backupCodes.map((code, index) => (
<code <code
@@ -44,6 +44,7 @@ export const UserNav = () => {
> >
<Avatar className="h-8 w-8 rounded-lg"> <Avatar className="h-8 w-8 rounded-lg">
<AvatarImage <AvatarImage
className="object-cover"
src={data?.user?.image || ""} src={data?.user?.image || ""}
alt={data?.user?.image || ""} alt={data?.user?.image || ""}
/> />
@@ -39,13 +39,19 @@ export function AlertBlock({
<div <div
{...props} {...props}
className={cn( className={cn(
"flex items-center flex-row gap-4 rounded-lg p-2", "flex items-start flex-row gap-4 rounded-lg p-2",
iconClassName, iconClassName,
className, className,
)} )}
> >
{icon || <Icon className="text-current" />} <div className="flex-shrink-0 mt-0.5">
<span className="text-sm text-current">{children}</span> {icon || <Icon className="text-current" />}
</div>
<div className="flex-1 min-w-0">
<span className="text-sm text-current break-words overflow-wrap-anywhere whitespace-pre-wrap">
{children}
</span>
</div>
</div> </div>
); );
} }
+3
View File
@@ -55,6 +55,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
ref, ref,
) => { ) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : "button";
const type = props.type ?? undefined;
return ( return (
<> <>
<Comp <Comp
@@ -65,6 +67,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
ref={ref} ref={ref}
{...props} {...props}
disabled={isLoading || props.disabled} disabled={isLoading || props.disabled}
type={type}
> >
{isLoading && <Loader2 className="animate-spin" />} {isLoading && <Loader2 className="animate-spin" />}
<Slottable>{children}</Slottable> <Slottable>{children}</Slottable>
@@ -14,10 +14,9 @@ import {
PlusIcon, PlusIcon,
Search, Search,
ServerIcon, ServerIcon,
SquareTerminal,
Trash2, Trash2,
X, X,
Terminal,
SquareTerminal,
} from "lucide-react"; } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
@@ -35,8 +34,8 @@ import { AddDatabase } from "@/components/dashboard/project/add-database";
import { AddTemplate } from "@/components/dashboard/project/add-template"; import { AddTemplate } from "@/components/dashboard/project/add-template";
import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector"; import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project"; import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
import { ProjectEnvironment } from "@/components/dashboard/projects/project-environment"; import { ProjectEnvironment } from "@/components/dashboard/projects/project-environment";
import { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -49,6 +48,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -98,8 +98,6 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
export type Services = { export type Services = {
appName: string; appName: string;
@@ -781,7 +779,7 @@ const EnvironmentPage = (
currentEnvironmentId={environmentId} currentEnvironmentId={environmentId}
/> />
<EnvironmentVariables environmentId={environmentId}> <EnvironmentVariables environmentId={environmentId}>
<SquareTerminal className="h-5 w-5 text-muted-foreground" /> <SquareTerminal className="h-5 w-5 text-muted-foreground cursor-pointer hover:text-primary transition-colors duration-200 hover:bg-primary/10 rounded-md p-px" />
</EnvironmentVariables> </EnvironmentVariables>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
+1 -1
View File
@@ -101,7 +101,7 @@ const Register = ({ isCloud }: Props) => {
setIsError(true); setIsError(true);
setError(error.message || "An error occurred"); setError(error.message || "An error occurred");
} else { } else {
toast.success("User registered successfuly", { toast.success("User registered successfully", {
duration: 2000, duration: 2000,
}); });
if (!isCloud) { if (!isCloud) {
+1 -1
View File
@@ -22,7 +22,7 @@ export const getGiteaOAuthUrl = (
} }
const redirectUri = `${baseUrl}/api/providers/gitea/callback`; const redirectUri = `${baseUrl}/api/providers/gitea/callback`;
const scopes = "repo repo:status read:user read:org"; const scopes = "read:repository read:user read:organization";
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent( return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
redirectUri, redirectUri,
+2 -2
View File
@@ -141,8 +141,8 @@ export function processValue(
} }
if ( if (
typeof payload === "string" && typeof payload === "string" &&
payload.startsWith("{") && payload.trimStart().startsWith("{") &&
payload.endsWith("}") payload.trimEnd().endsWith("}")
) { ) {
try { try {
payload = JSON.parse(payload); payload = JSON.parse(payload);
@@ -46,8 +46,14 @@ export const deleteMiddleware = (
}; };
export const deleteAllMiddlewares = async (application: ApplicationNested) => { export const deleteAllMiddlewares = async (application: ApplicationNested) => {
const config = loadMiddlewares<FileConfig>(); const { security, appName, redirects, serverId } = application;
const { security, appName, redirects } = application; let config: FileConfig;
if (serverId) {
config = await loadRemoteMiddlewares(serverId);
} else {
config = loadMiddlewares<FileConfig>();
}
if (config.http?.middlewares) { if (config.http?.middlewares) {
if (security.length > 0) { if (security.length > 0) {
@@ -62,8 +68,8 @@ export const deleteAllMiddlewares = async (application: ApplicationNested) => {
} }
} }
if (application.serverId) { if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", application.serverId); await writeTraefikConfigRemote(config, "middlewares", serverId);
} else { } else {
writeMiddleware(config); writeMiddleware(config);
} }