Merge branch 'canary' into 1345-domain-not-working-after-server-restart-or-traefik-reload

This commit is contained in:
Mauricio Siu
2025-03-09 01:12:04 -06:00
139 changed files with 46112 additions and 1272 deletions

View File

@@ -31,6 +31,8 @@ import {
FormDescription,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import copy from "copy-to-clipboard";
import { CodeEditor } from "@/components/shared/code-editor";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
@@ -441,13 +443,16 @@ export const AddApiKey = () => {
</DialogDescription>
</DialogHeader>
<div className="mt-4 space-y-4">
<div className="rounded-md bg-muted p-4 font-mono text-sm break-all">
{newApiKey}
</div>
<CodeEditor
className="font-mono text-sm break-all"
language="properties"
value={newApiKey}
readOnly
/>
<div className="flex justify-end gap-3">
<Button
onClick={() => {
navigator.clipboard.writeText(newApiKey);
copy(newApiKey);
toast.success("API key copied to clipboard");
}}
>

View File

@@ -39,12 +39,12 @@ import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({
name: z.string().min(1, "Name is required"),
provider: z.string().optional(),
accessKeyId: z.string(),
secretAccessKey: z.string(),
bucket: z.string(),
provider: z.string().min(1, "Provider is required"),
accessKeyId: z.string().min(1, "Access Key Id is required"),
secretAccessKey: z.string().min(1, "Secret Access Key is required"),
bucket: z.string().min(1, "Bucket is required"),
region: z.string(),
endpoint: z.string(),
endpoint: z.string().min(1, "Endpoint is required"),
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="" asChild>
@@ -349,26 +406,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
isLoading={isLoadingConnection}
onClick={async () => {
await testConnection({
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,
});
});
await handleTestConnection(form.getValues("serverId"));
}}
>
Test Connection
@@ -380,21 +420,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
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");
});
await handleTestConnection();
}}
>
Test connection

View File

@@ -56,9 +56,17 @@ export const ShowDestinations = () => {
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">
<span className="text-sm">
{index + 1}. {destination.name}
</span>
<div className="flex flex-col gap-1">
<span className="text-sm">
{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">
<HandleDestinations
destinationId={destination.destinationId}

View File

@@ -45,21 +45,12 @@ const Schema = z.object({
type Schema = z.infer<typeof Schema>;
interface Model {
id: string;
object: string;
created: number;
owned_by: string;
}
interface Props {
aiId?: string;
}
export const HandleAi = ({ aiId }: Props) => {
const [models, setModels] = useState<Model[]>([]);
const utils = api.useUtils();
const [isLoadingModels, setIsLoadingModels] = useState(false);
const [error, setError] = useState<string | null>(null);
const [open, setOpen] = useState(false);
const { data, refetch } = api.ai.one.useQuery(
@@ -73,6 +64,7 @@ export const HandleAi = ({ aiId }: Props) => {
const { mutateAsync, isLoading } = aiId
? api.ai.update.useMutation()
: api.ai.create.useMutation();
const form = useForm<Schema>({
resolver: zodResolver(Schema),
defaultValues: {
@@ -94,50 +86,33 @@ export const HandleAi = ({ aiId }: Props) => {
});
}, [aiId, form, data]);
const fetchModels = async (apiUrl: string, apiKey: string) => {
setIsLoadingModels(true);
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);
const apiUrl = form.watch("apiUrl");
const apiKey = form.watch("apiKey");
// Set default model to gpt-4 if present
const defaultModel = res.data.find(
(model: Model) => model.id === "gpt-4",
);
if (defaultModel) {
form.setValue("model", defaultModel.id);
return defaultModel.id;
}
} catch (error) {
setError("Failed to fetch models. Please check your API URL and Key.");
setModels([]);
} finally {
setIsLoadingModels(false);
}
};
const { data: models, isLoading: isLoadingServerModels } =
api.ai.getModels.useQuery(
{
apiUrl: apiUrl ?? "",
apiKey: apiKey ?? "",
},
{
enabled: !!apiUrl && !!apiKey,
onError: (error) => {
setError(`Failed to fetch models: ${error.message}`);
},
},
);
useEffect(() => {
const apiUrl = form.watch("apiUrl");
const apiKey = form.watch("apiKey");
if (apiUrl && apiKey) {
form.setValue("model", "");
fetchModels(apiUrl, apiKey);
}
}, [form.watch("apiUrl"), form.watch("apiKey")]);
const onSubmit = async (data: Schema) => {
try {
console.log("Form data:", data);
console.log("Current model value:", form.getValues("model"));
await mutateAsync({
...data,
aiId: aiId || "",
@@ -148,8 +123,9 @@ export const HandleAi = ({ aiId }: Props) => {
refetch();
setOpen(false);
} 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">
Loading models...
</span>
)}
{!isLoadingModels && models.length > 0 && (
{!isLoadingServerModels && models && models.length > 0 && (
<FormField
control={form.control}
name="model"

View File

@@ -139,6 +139,15 @@ export const ValidateServer = ({ serverId }: Props) => {
: "Not Created"
}
/>
<StatusRow
label="Railpack Installed"
isEnabled={data?.railpack?.enabled}
description={
data?.railpack?.enabled
? `Installed: ${data?.railpack?.version}`
: undefined
}
/>
</div>
</div>
</div>

View File

@@ -35,7 +35,7 @@ const addServerDomain = z
.object({
domain: z.string().min(1, { message: "URL is required" }),
letsEncryptEmail: z.string(),
certificateType: z.enum(["letsencrypt", "none"]),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
})
.superRefine((data, ctx) => {
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
@@ -193,6 +193,7 @@ export const WebDomain = () => {
);
}}
/>
<div className="flex w-full justify-end col-span-2">
<Button isLoading={isLoading} type="submit">
{t("settings.common.save")}

View File

@@ -5,6 +5,12 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import type { IUpdateData } from "@dokploy/server/index";
import {
@@ -24,9 +30,17 @@ import { UpdateWebServer } from "./update-webserver";
interface Props {
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 [isUpdateAvailable, setIsUpdateAvailable] = useState(
!!updateData?.updateAvailable,
@@ -35,10 +49,10 @@ export const UpdateServer = ({ updateData }: Props) => {
api.settings.getUpdateData.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
const [isOpen, setIsOpen] = useState(false);
const [latestVersion, setLatestVersion] = useState(
updateData?.latestVersion ?? "",
);
const [isOpenInternal, setIsOpenInternal] = useState(false);
const handleCheckUpdates = async () => {
try {
@@ -65,28 +79,52 @@ export const UpdateServer = ({ updateData }: Props) => {
}
};
const isOpen = isOpenInternal || isOpenProp;
const onOpenChange = (open: boolean) => {
setIsOpenInternal(open);
onOpenChangeProp?.(open);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button
variant={updateData ? "outline" : "secondary"}
className="gap-2"
>
{updateData ? (
<>
<span className="flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
Update available
</>
) : (
<>
<Sparkles className="h-4 w-4" />
Updates
</>
)}
</Button>
{children ? (
children
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={updateData ? "outline" : "secondary"}
size="sm"
onClick={() => onOpenChange?.(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>
)}
</DialogTrigger>
<DialogContent className="max-w-lg p-6">
<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="flex items-center gap-2">
<Button variant="outline" onClick={() => setIsOpen(false)}>
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
Cancel
</Button>
{isUpdateAvailable ? (