mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-31 09:12:15 +00:00
Merge branch 'canary' into fix/structured-log-level
This commit is contained in:
+1
-1
@@ -156,7 +156,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
|
||||
<div className="flex gap-4 h-[60vh] py-4">
|
||||
{/* Left Column - Menu */}
|
||||
<div className="w-64 flex-shrink-0 border-r pr-4 overflow-y-auto">
|
||||
<div className="w-64 shrink-0 border-r pr-4 overflow-y-auto">
|
||||
<nav className="space-y-1">
|
||||
<TooltipProvider>
|
||||
{menuItems.map((item) => (
|
||||
|
||||
@@ -185,7 +185,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog open={showModal} onOpenChange={setShowModal}>
|
||||
<DialogContent className="max-w-[50vw]">
|
||||
<DialogContent className="sm:max-w-3xl max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
Template Information
|
||||
@@ -199,7 +199,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-6 flex-1 min-h-0 overflow-y-auto pr-1">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code2 className="h-5 w-5 text-primary" />
|
||||
@@ -207,12 +207,14 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
Docker Compose
|
||||
</h3>
|
||||
</div>
|
||||
<CodeEditor
|
||||
language="yaml"
|
||||
value={templateInfo?.compose || ""}
|
||||
className="font-mono"
|
||||
readOnly
|
||||
/>
|
||||
<div className="max-h-[45vh] overflow-auto rounded-md border">
|
||||
<CodeEditor
|
||||
language="yaml"
|
||||
value={templateInfo?.compose || ""}
|
||||
className="font-mono"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
@@ -229,7 +231,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
(domain, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm"
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-xs"
|
||||
>
|
||||
<div className="font-medium">
|
||||
{domain.serviceName}
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ export const HandleRedirect = ({
|
||||
control={form.control}
|
||||
name="permanent"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Permanent</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 relative">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<div className="flex flex-col gap-6 max-h-140 min-h-40 overflow-y-auto">
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
value={data || "Empty"}
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
wrapperClassName="h-140 font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
|
||||
@@ -220,7 +220,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="bind"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
>
|
||||
Bind Mount
|
||||
</Label>
|
||||
@@ -240,7 +240,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="volume"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
>
|
||||
Volume Mount
|
||||
</Label>
|
||||
@@ -264,7 +264,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="file"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
>
|
||||
File Mount
|
||||
</Label>
|
||||
@@ -324,7 +324,7 @@ export const AddVolumes = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="max-w-full max-w-[45rem]">
|
||||
<FormItem className="max-w-full max-w-180">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -111,7 +111,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
||||
{mount.type === "file" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Content</span>
|
||||
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
|
||||
<span className="text-sm text-muted-foreground line-clamp-10 whitespace-break-spaces">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -253,7 +253,7 @@ export const UpdateVolume = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full max-w-[45rem]">
|
||||
<FormItem className="w-full max-w-180">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -191,7 +191,7 @@ export const ShowDeployment = ({
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-background rounded custom-logs-scrollbar"
|
||||
>
|
||||
{" "}
|
||||
{filteredLogs.length > 0 ? (
|
||||
|
||||
@@ -63,6 +63,9 @@ export const ShowDeployments = ({
|
||||
const [activeLog, setActiveLog] = useState<
|
||||
RouterOutputs["deployment"]["all"][number] | null
|
||||
>(null);
|
||||
const [removingDeploymentIds, setRemovingDeploymentIds] = useState<
|
||||
Set<string>
|
||||
>(new Set());
|
||||
const { data: deployments, isPending: isLoadingDeployments } =
|
||||
api.deployment.allByType.useQuery(
|
||||
{
|
||||
@@ -81,7 +84,7 @@ export const ShowDeployments = ({
|
||||
api.rollback.rollback.useMutation();
|
||||
const { mutateAsync: killProcess, isPending: isKillingProcess } =
|
||||
api.deployment.killProcess.useMutation();
|
||||
const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
|
||||
const { mutateAsync: removeDeployment } =
|
||||
api.deployment.removeDeployment.useMutation();
|
||||
|
||||
// Cancel deployment mutations
|
||||
@@ -147,7 +150,7 @@ export const ShowDeployments = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="bg-background border-none">
|
||||
<Card className="bg-background border-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||
@@ -233,7 +236,6 @@ export const ShowDeployments = ({
|
||||
<span>Webhook URL: </span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Badge
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Copy webhook URL to clipboard"
|
||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer whitespace-normal break-all"
|
||||
@@ -301,7 +303,7 @@ export const ShowDeployments = ({
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="break-words text-sm text-muted-foreground whitespace-pre-wrap">
|
||||
<span className="wrap-break-word text-sm text-muted-foreground whitespace-pre-wrap">
|
||||
{isExpanded || !needsTruncation
|
||||
? titleText
|
||||
: truncateDescription(titleText)}
|
||||
@@ -409,6 +411,11 @@ export const ShowDeployments = ({
|
||||
description="Are you sure you want to delete this deployment? This action cannot be undone."
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setRemovingDeploymentIds((deploymentIds) => {
|
||||
const nextDeploymentIds = new Set(deploymentIds);
|
||||
nextDeploymentIds.add(deployment.deploymentId);
|
||||
return nextDeploymentIds;
|
||||
});
|
||||
try {
|
||||
await removeDeployment({
|
||||
deploymentId: deployment.deploymentId,
|
||||
@@ -416,13 +423,25 @@ export const ShowDeployments = ({
|
||||
toast.success("Deployment deleted successfully");
|
||||
} catch (error) {
|
||||
toast.error("Error deleting deployment");
|
||||
} finally {
|
||||
setRemovingDeploymentIds((deploymentIds) => {
|
||||
const nextDeploymentIds = new Set(
|
||||
deploymentIds,
|
||||
);
|
||||
nextDeploymentIds.delete(
|
||||
deployment.deploymentId,
|
||||
);
|
||||
return nextDeploymentIds;
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
isLoading={isRemovingDeployment}
|
||||
isLoading={removingDeploymentIds.has(
|
||||
deployment.deploymentId,
|
||||
)}
|
||||
>
|
||||
Delete
|
||||
<Trash2 className="size-4" />
|
||||
|
||||
@@ -209,7 +209,9 @@ export const createColumns = ({
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
{validationState?.error ? (
|
||||
{validationState?.isValid && validationState?.message ? (
|
||||
<p>{validationState.message}</p>
|
||||
) : validationState?.error ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium text-red-500">Error:</p>
|
||||
<p>{validationState.error}</p>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
INVALID_HOSTNAME_MESSAGE,
|
||||
VALID_HOSTNAME_REGEX,
|
||||
} from "@dokploy/server/utils/hostname-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { DatabaseZap, Dices, RefreshCw, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -53,7 +57,10 @@ export const domain = z
|
||||
.refine((val) => val === val.trim(), {
|
||||
message: "Domain name cannot have leading or trailing spaces",
|
||||
})
|
||||
.transform((val) => val.trim()),
|
||||
.transform((val) => val.trim())
|
||||
.refine((val) => VALID_HOSTNAME_REGEX.test(val), {
|
||||
message: INVALID_HOSTNAME_MESSAGE,
|
||||
}),
|
||||
path: z.string().min(1).optional(),
|
||||
internalPath: z.string().optional(),
|
||||
stripPath: z.boolean().optional(),
|
||||
@@ -349,10 +356,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
{domainType === "compose" && (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
<AlertBlock type="warning" className="wrap-anywhere">
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
@@ -420,7 +424,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and
|
||||
@@ -450,7 +454,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
@@ -488,7 +492,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
{isManualInput
|
||||
@@ -565,7 +569,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>Generate sslip.io domain</p>
|
||||
</TooltipContent>
|
||||
@@ -618,7 +622,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
control={form.control}
|
||||
name="stripPath"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Strip Path</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -662,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
control={form.control}
|
||||
name="useCustomEntrypoint"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Custom Entrypoint</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -711,7 +715,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -763,6 +767,37 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<SelectItem value={"custom"}>Custom</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
{field.value === "none" && (
|
||||
<>
|
||||
<strong>None</strong> serves TLS using any
|
||||
certificate you created in the{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/certificates"
|
||||
className="text-primary"
|
||||
>
|
||||
Certificates
|
||||
</Link>{" "}
|
||||
section whose CN/SAN matches this host —
|
||||
Traefik selects it automatically via SNI.
|
||||
</>
|
||||
)}
|
||||
{field.value === "letsencrypt" && (
|
||||
<>
|
||||
<strong>Let's Encrypt</strong> auto-provisions
|
||||
a certificate automatically for this host.
|
||||
</>
|
||||
)}
|
||||
{field.value === "custom" && (
|
||||
<>
|
||||
<strong>Custom</strong> uses a Traefik cert
|
||||
resolver by name (defined in your static
|
||||
configuration).
|
||||
</>
|
||||
)}
|
||||
{!field.value &&
|
||||
"Select a certificate provider to see how TLS will be served for this host."}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
@@ -777,10 +812,19 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Custom Certificate Resolver</FormLabel>
|
||||
<FormDescription>
|
||||
Enter the <strong>name</strong> of a Traefik
|
||||
cert resolver defined in your static
|
||||
configuration (e.g. <code>letsencrypt</code>) —
|
||||
not certificate or private key content. To use a
|
||||
certificate you pasted in the Certificates
|
||||
section, choose <strong>None</strong> instead
|
||||
and Traefik will match it by SNI.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="w-full"
|
||||
placeholder="Enter your custom certificate resolver"
|
||||
placeholder="e.g. letsencrypt"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -626,7 +626,10 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
{validationState?.error ? (
|
||||
{validationState?.isValid &&
|
||||
validationState?.message ? (
|
||||
<p>{validationState.message}</p>
|
||||
) : validationState?.error ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium text-red-500">
|
||||
Error:
|
||||
|
||||
@@ -60,14 +60,16 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const currentBuildArgs = form.watch("buildArgs");
|
||||
const currentBuildSecrets = form.watch("buildSecrets");
|
||||
const currentCreateEnvFile = form.watch("createEnvFile");
|
||||
const { isDirty } = form.formState;
|
||||
const hasChanges =
|
||||
currentEnv !== (data?.env || "") ||
|
||||
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||
currentBuildSecrets !== (data?.buildSecrets || "") ||
|
||||
currentCreateEnvFile !== (data?.createEnvFile ?? true);
|
||||
|
||||
// Skip reset while editing so background refetches don't wipe edits
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && !isDirty) {
|
||||
form.reset({
|
||||
env: data.env || "",
|
||||
buildArgs: data.buildArgs || "",
|
||||
@@ -75,7 +77,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
createEnvFile: data.createEnvFile ?? true,
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
}, [data, isDirty, form]);
|
||||
|
||||
const onSubmit = async (formData: EnvironmentSchema) => {
|
||||
mutateAsync({
|
||||
@@ -87,6 +89,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Environments Added");
|
||||
form.reset(formData);
|
||||
await refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -189,7 +192,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="createEnvFile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Environment File</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
+31
-16
@@ -188,6 +188,9 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Bitbucket Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -196,7 +199,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -245,7 +247,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -254,14 +256,19 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
(repo) =>
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username === field.value.owner,
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -281,7 +288,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.name}
|
||||
value={`${repo.owner.username}/${repo.name}`}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -292,8 +299,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
@@ -301,7 +308,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username === field.value.owner
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
@@ -333,7 +341,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
" w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -348,7 +356,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
@@ -376,7 +387,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<span className="truncate">{branch.name}</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -436,14 +447,18 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -491,14 +506,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -242,14 +242,18 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -298,14 +302,14 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+33
-16
@@ -201,6 +201,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Gitea Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -208,7 +211,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -258,7 +260,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -268,14 +270,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo: GiteaRepository) =>
|
||||
repo.name === field.value.repo,
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username === field.value.owner,
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -301,7 +307,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
{repositories?.map((repo: GiteaRepository) => {
|
||||
return (
|
||||
<CommandItem
|
||||
value={repo.name}
|
||||
value={`${repo.owner.username}/${repo.name}`}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -311,8 +317,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">
|
||||
{repo.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
@@ -320,7 +328,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username ===
|
||||
field.value.owner
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
@@ -353,7 +363,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
" w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -369,7 +379,10 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
@@ -400,7 +413,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<span className="truncate">{branch.name}</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -464,14 +477,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{path}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
field.onChange(newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -518,14 +535,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+48
-21
@@ -177,6 +177,9 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Github Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -189,7 +192,14 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Github Account" />
|
||||
<SelectValue placeholder="Select a Github Account">
|
||||
{
|
||||
githubProviders?.find(
|
||||
(githubProvider) =>
|
||||
githubProvider.githubId === field.value,
|
||||
)?.gitProvider.name
|
||||
}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -233,7 +243,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -242,14 +252,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name ?? "Select repository")}
|
||||
(repo) =>
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.login === field.value.owner,
|
||||
)?.name ?? field.value.repo)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -269,7 +284,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.name}
|
||||
value={`${repo.owner.login}/${repo.name}`}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -279,8 +294,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.login}
|
||||
</span>
|
||||
@@ -288,7 +303,8 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.login === field.value.owner
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
@@ -320,22 +336,25 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
" w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "pending" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
? (branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
)?.name ?? field.value)
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
@@ -363,7 +382,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<span className="truncate">{branch.name}</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -419,8 +438,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -468,14 +491,18 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{path}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
field.onChange(newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -524,14 +551,14 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+27
-13
@@ -196,6 +196,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<FormLabel>Gitlab Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -205,7 +208,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -254,7 +256,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -270,7 +272,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -308,8 +313,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">
|
||||
{repo.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
@@ -351,7 +358,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
" w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -366,7 +373,10 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
@@ -394,7 +404,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<span className="truncate">{branch.name}</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -457,14 +467,18 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
{path}
|
||||
<X
|
||||
className="size-3 cursor-pointer hover:text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
field.onChange(newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="size-3 cursor-pointer hover:text-destructive" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -511,14 +525,14 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -154,7 +154,10 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="flex gap-4 justify-start bg-transparent"
|
||||
>
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { toast } from "sonner";
|
||||
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
|
||||
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
|
||||
@@ -94,7 +94,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Downloads the source code and performs a complete
|
||||
build
|
||||
@@ -137,7 +137,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Reload the application without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -176,7 +176,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Only rebuilds the application without downloading new
|
||||
code
|
||||
@@ -219,7 +219,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Start the application (requires a previous successful
|
||||
build)
|
||||
@@ -259,7 +259,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Stop the currently running application</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -271,6 +271,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
serviceId={applicationId}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import { GlobeIcon, Pencil, Search, X } from "lucide-react";
|
||||
import { CircuitBoard, GlobeIcon, Pencil, Search, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -16,7 +16,8 @@ import { type BundledIcon, bundledIcons } from "@/lib/bundled-icons";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface ShowIconSettingsProps {
|
||||
applicationId: string;
|
||||
serviceId: string;
|
||||
serviceType: "application" | "compose";
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
@@ -26,7 +27,8 @@ const svgToDataUrl = (icon: BundledIcon): string => {
|
||||
};
|
||||
|
||||
export const ShowIconSettings = ({
|
||||
applicationId,
|
||||
serviceId,
|
||||
serviceType,
|
||||
icon,
|
||||
}: ShowIconSettingsProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -48,6 +50,17 @@ export const ShowIconSettings = ({
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync: updateApplication } =
|
||||
api.application.update.useMutation();
|
||||
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
|
||||
|
||||
const updateIcon = async (newIcon: string | null) => {
|
||||
if (serviceType === "compose") {
|
||||
await updateCompose({ composeId: serviceId, icon: newIcon });
|
||||
await utils.compose.one.invalidate({ composeId: serviceId });
|
||||
} else {
|
||||
await updateApplication({ applicationId: serviceId, icon: newIcon });
|
||||
await utils.application.one.invalidate({ applicationId: serviceId });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -59,12 +72,8 @@ export const ShowIconSettings = ({
|
||||
const handleIconSelect = async (selectedIcon: BundledIcon) => {
|
||||
try {
|
||||
const dataUrl = svgToDataUrl(selectedIcon);
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: dataUrl,
|
||||
});
|
||||
await updateIcon(dataUrl);
|
||||
toast.success("Icon saved successfully");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
setOpen(false);
|
||||
} catch (_error) {
|
||||
toast.error("Error saving icon");
|
||||
@@ -73,12 +82,8 @@ export const ShowIconSettings = ({
|
||||
|
||||
const handleRemoveIcon = async () => {
|
||||
try {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: null,
|
||||
});
|
||||
await updateIcon(null);
|
||||
toast.success("Icon removed");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
} catch (_error) {
|
||||
toast.error("Error removing icon");
|
||||
}
|
||||
@@ -130,12 +135,8 @@ export const ShowIconSettings = ({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: sanitizedDataUrl,
|
||||
});
|
||||
await updateIcon(sanitizedDataUrl);
|
||||
toast.success("Icon saved!");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
setOpen(false);
|
||||
} catch (_error) {
|
||||
toast.error("Error saving icon");
|
||||
@@ -147,12 +148,8 @@ export const ShowIconSettings = ({
|
||||
reader.onload = async (event) => {
|
||||
const result = event.target?.result as string;
|
||||
try {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
icon: result,
|
||||
});
|
||||
await updateIcon(result);
|
||||
toast.success("Icon saved!");
|
||||
await utils.application.one.invalidate({ applicationId });
|
||||
setOpen(false);
|
||||
} catch (_error) {
|
||||
toast.error("Error saving icon");
|
||||
@@ -172,9 +169,11 @@ export const ShowIconSettings = ({
|
||||
// biome-ignore lint/performance/noImgElement: icon is data URL or base64
|
||||
<img
|
||||
src={icon}
|
||||
alt="Application icon"
|
||||
alt="Service icon"
|
||||
className="h-8 w-8 object-contain"
|
||||
/>
|
||||
) : serviceType === "compose" ? (
|
||||
<CircuitBoard className="h-6 w-6 text-muted-foreground" />
|
||||
) : (
|
||||
<GlobeIcon className="h-6 w-6 text-muted-foreground" />
|
||||
)}
|
||||
|
||||
@@ -50,9 +50,10 @@ export const badgeStateColor = (state: string) => {
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
export const ShowDockerLogs = ({ appName, serverId, serviceId }: Props) => {
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
|
||||
@@ -182,6 +183,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
+2
-2
@@ -200,7 +200,7 @@ export const AddPreviewDomain = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>Generate sslip.io domain</p>
|
||||
</TooltipContent>
|
||||
@@ -249,7 +249,7 @@ export const AddPreviewDomain = ({
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
+4
-4
@@ -1,4 +1,3 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
RocketIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { toast } from "sonner";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
@@ -132,7 +132,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
|
||||
<GitPullRequest className="size-5 text-muted-foreground mt-1 shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-sm">
|
||||
{deployment.pullRequestTitle}
|
||||
@@ -152,7 +152,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
<div className="pl-8 space-y-3">
|
||||
<div className="relative flex-grow">
|
||||
<div className="relative grow">
|
||||
<Input
|
||||
value={deploymentUrl}
|
||||
readOnly
|
||||
@@ -244,7 +244,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="z-[60]"
|
||||
className="z-60"
|
||||
>
|
||||
<p>
|
||||
Rebuild the preview deployment without
|
||||
|
||||
+2
-2
@@ -325,7 +325,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="previewHttps"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -431,7 +431,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="previewRequireCollaboratorPermissions"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs col-span-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
Require Collaborator Permissions
|
||||
|
||||
@@ -355,10 +355,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
{scheduleTypeForm === "compose" && (
|
||||
<div className="flex flex-col w-full gap-4">
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
<AlertBlock type="warning" className="wrap-anywhere">
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
@@ -414,7 +411,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -444,7 +441,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this compose,
|
||||
@@ -534,7 +531,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -58,8 +58,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
const handleRunManually = async (scheduleId: string) => {
|
||||
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
||||
try {
|
||||
await runManually({ scheduleId });
|
||||
toast.success("Schedule run successfully");
|
||||
const result = await runManually({ scheduleId });
|
||||
if (result.status === "error") {
|
||||
toast.error("Schedule run failed, check the deployment logs");
|
||||
} else {
|
||||
toast.success("Schedule run successfully");
|
||||
}
|
||||
await refetchSchedules();
|
||||
} catch {
|
||||
toast.error("Error running schedule");
|
||||
@@ -73,7 +77,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
<Card className=" px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
<CardHeader className="px-0">
|
||||
<div className="flex justify-between items-center gap-y-2 flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -110,12 +114,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
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 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 shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<Clock className="size-4 text-primary/70" />
|
||||
</div>
|
||||
<div className="space-y-1.5 w-full sm:w-auto">
|
||||
<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 wrap-anywhere line-clamp-3">
|
||||
{schedule.name}
|
||||
</h3>
|
||||
<Badge
|
||||
@@ -126,7 +130,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
</Badge>
|
||||
</div>
|
||||
{schedule.description && (
|
||||
<p className="text-xs text-muted-foreground/70 [overflow-wrap:anywhere] line-clamp-2">
|
||||
<p className="text-xs text-muted-foreground/70 wrap-anywhere line-clamp-2">
|
||||
{schedule.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -154,7 +158,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
</div>
|
||||
{schedule.command && (
|
||||
<div className="flex items-start gap-2 max-w-full">
|
||||
<Terminal className="size-3.5 text-muted-foreground/70 flex-shrink-0 mt-0.5" />
|
||||
<Terminal className="size-3.5 text-muted-foreground/70 shrink-0 mt-0.5" />
|
||||
<code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
|
||||
{schedule.command}
|
||||
</code>
|
||||
|
||||
+16
-10
@@ -349,10 +349,7 @@ export const HandleVolumeBackups = ({
|
||||
<>
|
||||
<div className="flex flex-col w-full gap-4">
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
<AlertBlock type="warning" className="wrap-anywhere">
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
@@ -408,7 +405,7 @@ export const HandleVolumeBackups = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -438,7 +435,7 @@ export const HandleVolumeBackups = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
@@ -510,11 +507,20 @@ export const HandleVolumeBackups = ({
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{mounts?.map((mount) => (
|
||||
<SelectItem key={mount.Name} value={mount.Name || ""}>
|
||||
{mount.Name}
|
||||
{mounts && mounts.length > 0 ? (
|
||||
mounts.map((mount) => (
|
||||
<SelectItem
|
||||
key={mount.Name}
|
||||
value={mount.Name || ""}
|
||||
>
|
||||
{mount.Name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" disabled>
|
||||
No volumes found
|
||||
</SelectItem>
|
||||
))}
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
|
||||
+2
-2
@@ -181,7 +181,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -263,7 +263,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export const ShowVolumeBackups = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
<Card className=" px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
<CardHeader className="px-0">
|
||||
<div className="flex justify-between items-center flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -111,7 +112,10 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Enable Isolated Deployment</CardTitle>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
Enable Isolated Deployment
|
||||
<Badge variant="yellow">Deprecated</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure isolated deployment to the compose file.
|
||||
<div className="text-sm text-muted-foreground flex flex-col gap-2">
|
||||
@@ -138,6 +142,11 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<AlertBlock type="warning">
|
||||
Isolated deployment is deprecated. Use the Networks section above to
|
||||
attach networks per service and detach them from dokploy-network —
|
||||
it is declarative and does not break on restarts.
|
||||
</AlertBlock>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -160,7 +169,7 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="isolatedDeployment"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
Enable Isolated Deployment ({data?.appName})
|
||||
|
||||
@@ -55,12 +55,14 @@ interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowComposeContainers = ({
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const { data, isPending, refetch } =
|
||||
api.docker.getContainersByAppNameMatch.useQuery(
|
||||
@@ -122,6 +124,7 @@ export const ShowComposeContainers = ({
|
||||
key={container.containerId}
|
||||
container={container}
|
||||
serverId={serverId}
|
||||
serviceId={serviceId}
|
||||
onActionComplete={() => refetch()}
|
||||
/>
|
||||
))}
|
||||
@@ -142,12 +145,14 @@ interface ContainerRowProps {
|
||||
status: string;
|
||||
};
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
onActionComplete: () => void;
|
||||
}
|
||||
|
||||
const ContainerRow = ({
|
||||
container,
|
||||
serverId,
|
||||
serviceId,
|
||||
onActionComplete,
|
||||
}: ContainerRowProps) => {
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
@@ -236,6 +241,7 @@ const ContainerRow = ({
|
||||
<DockerTerminalModal
|
||||
containerId={container.containerId}
|
||||
serverId={serverId || ""}
|
||||
serviceId={serviceId}
|
||||
>
|
||||
Terminal
|
||||
</DockerTerminalModal>
|
||||
@@ -280,6 +286,7 @@ const ContainerRow = ({
|
||||
containerId={container.containerId}
|
||||
serverId={serverId}
|
||||
runType="native"
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -72,7 +72,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Downloads the source code and performs a complete build
|
||||
</p>
|
||||
@@ -84,19 +84,19 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
)}
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
title="Reload Compose"
|
||||
description="Are you sure you want to reload this compose?"
|
||||
title="Rebuild Compose"
|
||||
description="Are you sure you want to rebuild this compose?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await redeploy({
|
||||
composeId: composeId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Compose reloaded successfully");
|
||||
toast.success("Compose rebuilt successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading compose");
|
||||
toast.error("Error rebuilding compose");
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -109,12 +109,14 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
Rebuild
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Reload the compose without rebuilding it</p>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Rebuilds the compose without downloading the source code
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
@@ -154,7 +156,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Start the compose (requires a previous successful build)
|
||||
</p>
|
||||
@@ -193,7 +195,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Stop the currently running compose</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -206,6 +208,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
serviceId={data?.composeId}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -135,7 +135,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="overflow-auto">
|
||||
<FormControl className="">
|
||||
<div className="flex flex-col gap-4 w-full outline-none focus:outline-none overflow-auto">
|
||||
<div className="flex flex-col gap-4 w-full outline-hidden focus:outline-hidden overflow-auto">
|
||||
<CodeEditor
|
||||
// disabled
|
||||
language="yaml"
|
||||
|
||||
+31
-16
@@ -190,6 +190,9 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Bitbucket Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -198,7 +201,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -247,7 +249,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -256,14 +258,19 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
(repo) =>
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username === field.value.owner,
|
||||
)?.name ?? "Select repository")}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -283,7 +290,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.name}
|
||||
value={`${repo.owner.username}/${repo.name}`}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -294,8 +301,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
@@ -303,7 +310,8 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username === field.value.owner
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
@@ -335,7 +343,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
" w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -350,7 +358,10 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
@@ -378,7 +389,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<span className="truncate">{branch.name}</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -440,14 +451,18 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -495,14 +510,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+9
-5
@@ -251,14 +251,18 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -306,14 +310,14 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+34
-17
@@ -188,6 +188,9 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Gitea Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -195,7 +198,6 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -244,7 +246,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -253,13 +255,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
(repo) =>
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username === field.value.owner,
|
||||
)?.name ?? "Select repository")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -280,7 +287,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
key={repo.url}
|
||||
value={repo.name}
|
||||
value={`${repo.owner.username}/${repo.name}`}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
owner: repo.owner.username,
|
||||
@@ -289,8 +296,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
@@ -298,7 +305,8 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.username === field.value.owner
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
@@ -331,7 +339,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -346,7 +354,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branches..."
|
||||
@@ -363,8 +374,10 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", branch.name)
|
||||
}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{branch.name}
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">
|
||||
{branch.name}
|
||||
</span>
|
||||
</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
@@ -429,14 +442,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -484,14 +501,14 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+41
-22
@@ -138,7 +138,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
enableSubmodules: data.enableSubmodules ?? false,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data?.composeId, form]);
|
||||
}, [form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: GithubProvider) => {
|
||||
await mutateAsync({
|
||||
@@ -179,6 +179,9 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Github Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -186,7 +189,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -234,7 +236,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -243,14 +245,19 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
: isLoadingRepositories
|
||||
? "Loading...."
|
||||
: (repositories?.find(
|
||||
(repo) => repo.name === field.value.repo,
|
||||
)?.name ?? "Select repository")}
|
||||
(repo) =>
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.login === field.value.owner,
|
||||
)?.name ?? field.value.repo)}
|
||||
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -270,7 +277,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<CommandGroup>
|
||||
{repositories?.map((repo) => (
|
||||
<CommandItem
|
||||
value={repo.name}
|
||||
value={`${repo.owner.login}/${repo.name}`}
|
||||
key={repo.url}
|
||||
onSelect={() => {
|
||||
form.setValue("repository", {
|
||||
@@ -280,8 +287,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">{repo.name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.login}
|
||||
</span>
|
||||
@@ -289,7 +296,8 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
repo.name === field.value.repo
|
||||
repo.name === field.value.repo &&
|
||||
repo.owner.login === field.value.owner
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
@@ -321,22 +329,25 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
" w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{status === "pending" && fetchStatus === "fetching"
|
||||
? "Loading...."
|
||||
: field.value
|
||||
? branches?.find(
|
||||
? (branches?.find(
|
||||
(branch) => branch.name === field.value,
|
||||
)?.name
|
||||
)?.name ?? field.value)
|
||||
: "Select branch"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
@@ -364,7 +375,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<span className="truncate">{branch.name}</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -421,8 +432,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -468,14 +483,18 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -527,14 +546,14 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
+27
-13
@@ -199,6 +199,9 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Gitlab Account</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
form.setValue("repository", {
|
||||
owner: "",
|
||||
@@ -208,7 +211,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
});
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -256,7 +258,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -272,7 +274,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search repository..."
|
||||
@@ -310,8 +315,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", "");
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{repo.name}</span>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">
|
||||
{repo.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{repo.owner.username}
|
||||
</span>
|
||||
@@ -353,7 +360,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between !bg-input",
|
||||
" w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -368,7 +375,10 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search branch..."
|
||||
@@ -396,7 +406,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
form.setValue("branch", branch.name);
|
||||
}}
|
||||
>
|
||||
{branch.name}
|
||||
<span className="truncate">{branch.name}</span>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
@@ -458,14 +468,18 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{path}
|
||||
<X
|
||||
className="ml-1 size-3 cursor-pointer"
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove watch path"
|
||||
className="inline-flex items-center focus-visible:ring-2"
|
||||
onClick={() => {
|
||||
const newPaths = [...(field.value || [])];
|
||||
newPaths.splice(index, 1);
|
||||
form.setValue("watchPaths", newPaths);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<X className="ml-1 size-3 cursor-pointer" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -513,14 +527,14 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="enableSubmodules"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
<FormLabel>Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -143,7 +143,10 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="flex gap-4 justify-start bg-transparent"
|
||||
>
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
|
||||
@@ -160,7 +160,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="randomize"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Apply Randomize</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
Preview Compose
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl max-h-[50rem]">
|
||||
<DialogContent className="sm:max-w-6xl max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Converted Compose</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -62,16 +62,12 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<AlertBlock type="info" className="mb-4">
|
||||
Preview your docker-compose file with added domains. Note: At least
|
||||
one domain must be specified for this conversion to take effect.
|
||||
</AlertBlock>
|
||||
{isPending ? (
|
||||
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
|
||||
<div className="flex flex-row items-center justify-center min-h-100 border p-4 rounded-md">
|
||||
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
|
||||
</div>
|
||||
) : compose?.length === 5 ? (
|
||||
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]">
|
||||
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-100">
|
||||
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
|
||||
<span className="text-muted-foreground">
|
||||
No converted compose data available.
|
||||
@@ -79,7 +75,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-row gap-2 justify-end my-4">
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isPending}
|
||||
@@ -100,14 +96,15 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<pre>
|
||||
<div className="flex-1 min-h-0 overflow-auto rounded-md border">
|
||||
<CodeEditor
|
||||
value={compose || ""}
|
||||
language="yaml"
|
||||
readOnly
|
||||
height="50rem"
|
||||
height="100%"
|
||||
wrapperClassName="h-full"
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
@@ -35,9 +35,14 @@ export const DockerLogs = dynamic(
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
export const ShowDockerLogsStack = ({
|
||||
appName,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
@@ -167,6 +172,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -35,12 +35,14 @@ interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogsCompose = ({
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
@@ -104,6 +106,7 @@ export const ShowDockerLogsCompose = ({
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType="native"
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -79,6 +79,7 @@ const Schema = z
|
||||
schedule: z.string().min(1, "Schedule (Cron) required"),
|
||||
prefix: z.string().min(1, "Prefix required"),
|
||||
enabled: z.boolean(),
|
||||
includeEncryptionKey: z.boolean(),
|
||||
database: z.string().min(1, "Database required"),
|
||||
keepLatestCount: z.coerce.number().optional(),
|
||||
serviceName: z.string().nullable(),
|
||||
@@ -223,6 +224,7 @@ export const HandleBackup = ({
|
||||
: "",
|
||||
destinationId: "",
|
||||
enabled: true,
|
||||
includeEncryptionKey: true,
|
||||
prefix: "/",
|
||||
schedule: "",
|
||||
keepLatestCount: undefined,
|
||||
@@ -262,6 +264,7 @@ export const HandleBackup = ({
|
||||
: "",
|
||||
destinationId: backup?.destinationId ?? "",
|
||||
enabled: backup?.enabled ?? true,
|
||||
includeEncryptionKey: backup?.includeEncryptionKey ?? true,
|
||||
prefix: backup?.prefix ?? "/",
|
||||
schedule: backup?.schedule ?? "",
|
||||
keepLatestCount: backup?.keepLatestCount ?? undefined,
|
||||
@@ -309,6 +312,7 @@ export const HandleBackup = ({
|
||||
prefix: data.prefix,
|
||||
schedule: data.schedule,
|
||||
enabled: data.enabled,
|
||||
includeEncryptionKey: data.includeEncryptionKey,
|
||||
database: data.database,
|
||||
keepLatestCount: data.keepLatestCount ?? null,
|
||||
databaseType: data.databaseType || databaseType,
|
||||
@@ -364,7 +368,7 @@ export const HandleBackup = ({
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{errorServices && (
|
||||
<AlertBlock type="warning" className="[overflow-wrap:anywhere]">
|
||||
<AlertBlock type="warning" className="wrap-anywhere">
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
@@ -409,7 +413,7 @@ export const HandleBackup = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -528,7 +532,7 @@ export const HandleBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -558,7 +562,7 @@ export const HandleBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
@@ -665,6 +669,31 @@ export const HandleBackup = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{databaseType === "web-server" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="includeEncryptionKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 ">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Include encryption key</FormLabel>
|
||||
<FormDescription>
|
||||
Stores the encryption key inside the backup so
|
||||
environment variables can be restored on a new server.
|
||||
Anyone with access to the backup file can decrypt
|
||||
them.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{backupType === "compose" && (
|
||||
<>
|
||||
{form.watch("databaseType") === "postgres" && (
|
||||
|
||||
@@ -345,7 +345,7 @@ export const RestoreBackup = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -427,7 +427,7 @@ export const RestoreBackup = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between !bg-input",
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -622,7 +622,7 @@ export const RestoreBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -652,7 +652,7 @@ export const RestoreBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
className="max-w-40"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this compose,
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||
</DialogHeader>
|
||||
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
|
||||
<code>
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
<pre className="whitespace-pre-wrap wrap-break-word">
|
||||
<CodeEditor
|
||||
language="json"
|
||||
lineWrapping
|
||||
|
||||
@@ -165,7 +165,7 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
||||
) : (
|
||||
<>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-sm break-words">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-sm wrap-break-word">
|
||||
<ReactMarkdown>{data.analysis}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
runType: "swarm" | "native";
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const priorities = [
|
||||
@@ -52,6 +53,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
containerId,
|
||||
serverId,
|
||||
runType,
|
||||
serviceId,
|
||||
}) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
@@ -157,6 +159,10 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
params.append("serverId", serverId);
|
||||
}
|
||||
|
||||
if (serviceId) {
|
||||
params.append("serviceId", serviceId);
|
||||
}
|
||||
|
||||
const wsUrl = `${protocol}//${
|
||||
window.location.host
|
||||
}/docker-container-logs?${params.toString()}`;
|
||||
@@ -222,7 +228,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
}, [containerId, serverId, lines, search, since]);
|
||||
}, [containerId, serverId, serviceId, lines, search, since]);
|
||||
|
||||
const handleDownload = () => {
|
||||
const logContent = filteredLogs
|
||||
|
||||
@@ -119,7 +119,7 @@ export function LineCountFilter({
|
||||
placeholder="Number of lines"
|
||||
value={inputValue}
|
||||
onValueChange={handleInputChange}
|
||||
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -146,7 +146,7 @@ export function LineCountFilter({
|
||||
<CommandPrimitive.Item
|
||||
key={option.value}
|
||||
onSelect={() => handleSelect(option.label)}
|
||||
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
@@ -65,22 +64,20 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
|
||||
const tooltip = (color: string, timestamp: string | null) => {
|
||||
const square = (
|
||||
<div className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)} />
|
||||
<div className={cn("w-2 h-full shrink-0 rounded-[3px]", color)} />
|
||||
);
|
||||
return timestamp ? (
|
||||
<TooltipProvider delayDuration={0} disableHoverableContent>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{square}</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="bg-popover border-border z-[99999]"
|
||||
>
|
||||
<p className="text text-xs text-muted-foreground break-all max-w-md">
|
||||
<pre>{timestamp}</pre>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="bg-popover border-border z-99999"
|
||||
>
|
||||
<p className="text text-xs text-muted-foreground break-all max-w-md">
|
||||
<pre>{timestamp}</pre>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
@@ -107,7 +104,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
||||
{tooltip(color, rawTimestamp)}
|
||||
{!noTimestamp && (
|
||||
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0">
|
||||
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 shrink-0">
|
||||
{formattedTime}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const RemoveContainerDialog = ({ containerId, serverId }: Props) => {
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
className="w-full cursor-pointer text-red-500 hover:text-red-600!"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Remove Container
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
@@ -37,6 +40,7 @@ export const columns: ColumnDef<Container>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "state",
|
||||
filterFn: "equals",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -56,7 +60,7 @@ export const columns: ColumnDef<Container>[] = [
|
||||
variant={
|
||||
value === "running"
|
||||
? "default"
|
||||
: value === "failed"
|
||||
: value === "exited" || value === "dead"
|
||||
? "destructive"
|
||||
: "secondary"
|
||||
}
|
||||
@@ -99,6 +103,28 @@ export const columns: ColumnDef<Container>[] = [
|
||||
},
|
||||
cell: ({ row }) => <div className="lowercase">{row.getValue("image")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "ports",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Ports
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("ports") as string;
|
||||
return (
|
||||
<div className="max-w-[16rem] truncate lowercase" title={value}>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
@@ -115,6 +141,14 @@ export const columns: ColumnDef<Container>[] = [
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
copy(container.containerId);
|
||||
toast.success("Container ID copied to clipboard");
|
||||
}}
|
||||
>
|
||||
Copy Container ID
|
||||
</DropdownMenuItem>
|
||||
<ShowDockerModalLogs
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
useReactTable,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table";
|
||||
import { ChevronDown, Container } from "lucide-react";
|
||||
import { ChevronDown, Container, RefreshCw } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -26,6 +26,13 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -44,10 +51,26 @@ interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
const CONTAINER_STATES = [
|
||||
"running",
|
||||
"exited",
|
||||
"paused",
|
||||
"restarting",
|
||||
"created",
|
||||
"removing",
|
||||
"dead",
|
||||
];
|
||||
|
||||
export const ShowContainers = ({ serverId }: Props) => {
|
||||
const { data, isPending } = api.docker.getContainers.useQuery({
|
||||
serverId,
|
||||
});
|
||||
const { data, isPending, refetch, isRefetching } =
|
||||
api.docker.getContainers.useQuery(
|
||||
{
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
refetchInterval: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
@@ -106,12 +129,48 @@ export const ShowContainers = ({ serverId }: Props) => {
|
||||
}
|
||||
className="md:max-w-sm"
|
||||
/>
|
||||
<Select
|
||||
value={
|
||||
(table.getColumn("state")?.getFilterValue() as string) ??
|
||||
"all"
|
||||
}
|
||||
onValueChange={(value) =>
|
||||
table
|
||||
.getColumn("state")
|
||||
?.setFilterValue(value === "all" ? undefined : value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40 max-sm:w-full capitalize">
|
||||
<SelectValue placeholder="Filter by state" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All states</SelectItem>
|
||||
{CONTAINER_STATES.map((state) => (
|
||||
<SelectItem
|
||||
key={state}
|
||||
value={state}
|
||||
className="capitalize"
|
||||
>
|
||||
{state}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 sm:ml-auto"
|
||||
onClick={() => refetch()}
|
||||
disabled={isRefetching}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${isRefetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span className="sr-only">Refresh</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="sm:ml-auto max-sm:w-full"
|
||||
>
|
||||
<Button variant="outline" className="max-sm:w-full">
|
||||
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -23,12 +23,14 @@ interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
children?: React.ReactNode;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminalModal = ({
|
||||
children,
|
||||
containerId,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const [mainDialogOpen, setMainDialogOpen] = useState(false);
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
@@ -74,6 +76,7 @@ export const DockerTerminalModal = ({
|
||||
id="terminal"
|
||||
containerId={containerId}
|
||||
serverId={serverId || ""}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
|
||||
|
||||
@@ -10,12 +10,14 @@ interface Props {
|
||||
id: string;
|
||||
containerId?: string;
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminal: React.FC<Props> = ({
|
||||
id,
|
||||
containerId,
|
||||
serverId,
|
||||
serviceId,
|
||||
}) => {
|
||||
const termRef = useRef(null);
|
||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||
@@ -38,7 +40,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
||||
const addonFit = new FitAddon();
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}${serviceId ? `&serviceId=${serviceId}` : ""}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full relative z-[5]"
|
||||
className="w-full relative z-5"
|
||||
>
|
||||
<div className="flex flex-col overflow-auto">
|
||||
{isLoadingFile ? (
|
||||
@@ -123,7 +123,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
wrapperClassName="h-140 font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
@@ -143,7 +143,7 @@ routers:
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
className="shadow-xs"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
||||
import {
|
||||
FileIcon,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Loader2,
|
||||
MousePointerClick,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import {
|
||||
@@ -68,19 +75,29 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No directories or files detected in{" "}
|
||||
{"'/etc/dokploy/traefik'"}
|
||||
</span>
|
||||
<Folder className="size-8 text-muted-foreground" />
|
||||
<div className="w-full flex-col gap-4 flex items-center justify-center h-[55vh] border border-dashed rounded-lg">
|
||||
<div className="flex items-center justify-center size-14 rounded-full bg-muted">
|
||||
<FolderOpen className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1 text-center px-4">
|
||||
<span className="text-base font-medium">
|
||||
No configuration files found
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
There are no directories or files in{" "}
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-xs">
|
||||
/etc/dokploy/traefik
|
||||
</code>{" "}
|
||||
on this server yet.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{directories && directories?.length > 0 && (
|
||||
<>
|
||||
<Tree
|
||||
data={directories}
|
||||
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
|
||||
className="lg:max-w-76 w-full lg:h-[660px] border rounded-lg"
|
||||
onSelectChange={(item) => setFile(item?.id || null)}
|
||||
folderIcon={Folder}
|
||||
itemIcon={Workflow}
|
||||
@@ -89,11 +106,19 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||
{file ? (
|
||||
<ShowTraefikFile path={file} serverId={serverId} />
|
||||
) : (
|
||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No file selected
|
||||
</span>
|
||||
<FileIcon className="size-8 text-muted-foreground" />
|
||||
<div className="h-full min-h-[300px] w-full flex-col gap-4 flex items-center justify-center border border-dashed rounded-lg">
|
||||
<div className="flex items-center justify-center size-14 rounded-full bg-muted">
|
||||
<MousePointerClick className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1 text-center px-4">
|
||||
<span className="text-base font-medium">
|
||||
Select a file to edit
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Choose a file from the tree on the left to view
|
||||
and edit its contents.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -197,7 +197,7 @@ export const ImpersonationBar = () => {
|
||||
>
|
||||
{selectedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<UserIcon className="mr-2 h-4 w-4 shrink-0" />
|
||||
<span className="truncate flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{`${selectedUser.name} ${selectedUser.lastName}`.trim() ||
|
||||
@@ -245,7 +245,7 @@ export const ImpersonationBar = () => {
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2 flex-1">
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<UserIcon className="h-4 w-4 shrink-0" />
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{`${user.name} ${user.lastName}`.trim() ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -96,7 +96,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Downloads and sets up the Libsql database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -136,7 +136,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Restart the Libsql service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -176,7 +176,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Start the Libsql database (requires a previous
|
||||
successful setup)
|
||||
@@ -218,7 +218,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Stop the currently running Libsql database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -229,6 +229,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
)}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.libsqlId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
@@ -243,7 +244,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Open a terminal to the Libsql container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
+3
-3
@@ -1,4 +1,3 @@
|
||||
import { SelectGroup } from "@radix-ui/react-select";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -6,6 +5,7 @@ import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
@@ -28,7 +28,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Sqld Node</Label>
|
||||
@@ -71,7 +71,7 @@ export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input disabled value={data?.appName} />
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Enable Namespaces</Label>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -99,7 +99,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Downloads and sets up the MariaDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -141,7 +141,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Restart the MariaDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -183,7 +183,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Start the MariaDB database (requires a previous
|
||||
successful setup)
|
||||
@@ -225,7 +225,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Stop the currently running MariaDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -236,6 +236,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
))}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mariadbId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
@@ -250,7 +251,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Open a terminal to the MariaDB container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
+3
-3
@@ -25,11 +25,11 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Database Name</Label>
|
||||
<Input disabled value={data?.databaseName} />
|
||||
<Input enableCopyButton disabled value={data?.databaseName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
@@ -79,7 +79,7 @@ export const ShowInternalMariadbCredentials = ({ mariadbId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input disabled value={data?.appName} />
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -99,7 +99,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Downloads and sets up the MongoDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -139,7 +139,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Restart the MongoDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -179,7 +179,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Start the MongoDB database (requires a previous
|
||||
successful setup)
|
||||
@@ -219,7 +219,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Stop the currently running MongoDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -230,6 +230,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mongoId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
@@ -244,7 +245,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Open a terminal to the MongoDB container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -55,7 +55,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input disabled value={data?.appName} />
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DockerCpuChart = ({ accumulativeData }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ export const DockerDiskUsageChart = () => {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[16rem]">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DockerMemoryChart = ({
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
+3
-3
@@ -227,7 +227,7 @@ export const ContainerFreeMonitoring = ({
|
||||
String(currentData.cpu.value ?? "0%").replace("%", ""),
|
||||
10,
|
||||
)}
|
||||
className="w-[100%]"
|
||||
className="w-full"
|
||||
/>
|
||||
<DockerCpuChart accumulativeData={accumulativeData.cpu} />
|
||||
</div>
|
||||
@@ -250,7 +250,7 @@ export const ContainerFreeMonitoring = ({
|
||||
convertMemoryToBytes(currentData.memory.value.total)) *
|
||||
100
|
||||
}
|
||||
className="w-[100%]"
|
||||
className="w-full"
|
||||
/>
|
||||
<DockerMemoryChart
|
||||
accumulativeData={accumulativeData.memory}
|
||||
@@ -275,7 +275,7 @@ export const ContainerFreeMonitoring = ({
|
||||
</span>
|
||||
<Progress
|
||||
value={currentData.disk.value.diskUsedPercentage}
|
||||
className="w-[100%]"
|
||||
className="w-full"
|
||||
/>
|
||||
<DockerDiskChart
|
||||
accumulativeData={accumulativeData.disk}
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ export const ContainerBlockChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -84,7 +84,7 @@ export const ContainerCPUChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ export const ContainerMemoryChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ export const ContainerNetworkChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -71,7 +71,7 @@ export function CPUChart({ data }: CPUChartProps) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -86,7 +86,7 @@ export function MemoryChart({ data }: MemoryChartProps) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -90,7 +90,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -202,7 +202,7 @@ export const ShowPaidMonitoring = ({
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">Uptime</h3>
|
||||
@@ -212,7 +212,7 @@ export const ShowPaidMonitoring = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">CPU Usage</h3>
|
||||
@@ -220,7 +220,7 @@ export const ShowPaidMonitoring = ({
|
||||
<p className="mt-2 text-2xl font-bold">{metrics.cpu}%</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border text-card-foreground bg-transparent shadow-sm p-6">
|
||||
<div className="rounded-lg border text-card-foreground bg-transparent shadow-xs p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">Memory Usage</h3>
|
||||
@@ -230,7 +230,7 @@ export const ShowPaidMonitoring = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">Disk Usage</h3>
|
||||
@@ -240,7 +240,7 @@ export const ShowPaidMonitoring = ({
|
||||
</div>
|
||||
|
||||
{/* System Information */}
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<h3 className="text-lg font-medium mb-4">System Information</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -97,7 +97,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Downloads and sets up the MySQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -137,7 +137,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Restart the MySQL service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -177,7 +177,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Start the MySQL database (requires a previous
|
||||
successful setup)
|
||||
@@ -217,7 +217,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Stop the currently running MySQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -228,6 +228,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mysqlId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
@@ -242,7 +243,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Open a terminal to the MySQL container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -25,11 +25,11 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Database Name</Label>
|
||||
<Input disabled value={data?.databaseName} />
|
||||
<Input enableCopyButton disabled value={data?.databaseName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
@@ -79,7 +79,7 @@ export const ShowInternalMysqlCredentials = ({ mysqlId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input disabled value={data?.appName} />
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Check, ChevronsUpDown, Loader2, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFieldArray, 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 {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Command,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const serviceSchema = z.object({
|
||||
serviceName: z.string(),
|
||||
networkIds: z.array(z.string()),
|
||||
detachDokployNetwork: z.boolean(),
|
||||
});
|
||||
|
||||
const formSchema = z.object({
|
||||
services: z.array(serviceSchema),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export const AssignComposeNetworks = ({ composeId }: Props) => {
|
||||
const [cacheType, setCacheType] = useState<"cache" | "fetch">("cache");
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||
const {
|
||||
data: services,
|
||||
isLoading: isLoadingServices,
|
||||
error: servicesError,
|
||||
refetch: refetchServices,
|
||||
isRefetching: isRefetchingServices,
|
||||
} = api.compose.loadServices.useQuery(
|
||||
{ composeId, type: cacheType },
|
||||
{ retry: false },
|
||||
);
|
||||
|
||||
const onRetry = () => {
|
||||
setCacheType("fetch");
|
||||
setTimeout(() => refetchServices(), 0);
|
||||
};
|
||||
|
||||
const { data: networks } = api.network.all.useQuery(
|
||||
{ serverId: compose?.serverId ?? undefined },
|
||||
{ enabled: compose !== undefined },
|
||||
);
|
||||
const { mutateAsync, isPending } = api.compose.update.useMutation();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { services: [] },
|
||||
});
|
||||
const { fields } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "services",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!services) return;
|
||||
const serviceNetworks = compose?.serviceNetworks ?? [];
|
||||
form.reset({
|
||||
services: services.map((serviceName) => {
|
||||
const config = serviceNetworks.find(
|
||||
(s) => s.serviceName === serviceName,
|
||||
);
|
||||
return {
|
||||
serviceName,
|
||||
networkIds: config?.networkIds ?? [],
|
||||
detachDokployNetwork: config?.detachDokployNetwork ?? false,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}, [services, compose?.serviceNetworks, form]);
|
||||
|
||||
const allowsBridge = compose?.composeType === "docker-compose";
|
||||
const availableNetworks = (networks ?? []).filter(
|
||||
(n) => allowsBridge || n.driver === "overlay",
|
||||
);
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
const serviceNetworks = values.services.filter(
|
||||
(s) => s.networkIds.length > 0 || s.detachDokployNetwork,
|
||||
);
|
||||
await mutateAsync({
|
||||
composeId,
|
||||
serviceNetworks,
|
||||
});
|
||||
toast.success("Networks updated. Redeploy the compose to apply them.");
|
||||
await utils.compose.one.invalidate({ composeId });
|
||||
} catch (error) {
|
||||
toast.error("Error updating networks", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-start justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">Networks</CardTitle>
|
||||
<CardDescription>
|
||||
Attach Docker networks per service and detach it from
|
||||
dokploy-network. Takes effect on the next deploy.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
isLoading={isRefetchingServices}
|
||||
onClick={onRetry}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{servicesError ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<AlertBlock type="error">
|
||||
Could not load the compose services. If this compose was just
|
||||
created from a template, it hasn't been cloned yet — click Reload
|
||||
to clone the repository and read its services.
|
||||
</AlertBlock>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
isLoading={isRefetchingServices}
|
||||
onClick={onRetry}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
Reload services
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : isLoadingServices ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center py-10 text-sm text-muted-foreground">
|
||||
<span>Loading services...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : !services?.length ? (
|
||||
<span className="py-6 text-center text-sm text-muted-foreground">
|
||||
No services found in this compose.
|
||||
</span>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
{fields.map((fieldItem, index) => (
|
||||
<ServiceRow
|
||||
key={fieldItem.id}
|
||||
control={form.control}
|
||||
index={index}
|
||||
service={fieldItem.serviceName}
|
||||
availableNetworks={availableNetworks}
|
||||
/>
|
||||
))}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!form.formState.isDirty}
|
||||
isLoading={isPending}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
type NetworkOption = { networkId: string; name: string; driver: string };
|
||||
|
||||
const ServiceRow = ({
|
||||
control,
|
||||
index,
|
||||
service,
|
||||
availableNetworks,
|
||||
}: {
|
||||
control: ReturnType<typeof useForm<FormValues>>["control"];
|
||||
index: number;
|
||||
service: string;
|
||||
availableNetworks: NetworkOption[];
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-lg border p-4">
|
||||
<FormField
|
||||
control={control}
|
||||
name={`services.${index}.detachDokployNetwork`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between gap-3 space-y-0">
|
||||
<span className="text-sm font-medium">{service}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Detach dokploy-network
|
||||
</span>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name={`services.${index}.networkIds`}
|
||||
render={({ field }) => {
|
||||
const selectedNetworks = availableNetworks.filter((n) =>
|
||||
field.value.includes(n.networkId),
|
||||
);
|
||||
const toggle = (networkId: string) => {
|
||||
field.onChange(
|
||||
field.value.includes(networkId)
|
||||
? field.value.filter((id) => id !== networkId)
|
||||
: [...field.value, networkId],
|
||||
);
|
||||
};
|
||||
return (
|
||||
<FormItem className="space-y-3">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{selectedNetworks.length > 0
|
||||
? `${selectedNetworks.length} network${
|
||||
selectedNetworks.length > 1 ? "s" : ""
|
||||
} selected`
|
||||
: "Select networks..."}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search networks..."
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList className="max-h-60">
|
||||
{availableNetworks.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
No networks available on this server.
|
||||
</div>
|
||||
) : (
|
||||
<CommandGroup>
|
||||
{availableNetworks.map((n) => {
|
||||
const isSelected = field.value.includes(
|
||||
n.networkId,
|
||||
);
|
||||
return (
|
||||
<CommandItem
|
||||
key={n.networkId}
|
||||
value={n.name}
|
||||
onSelect={() => toggle(n.networkId)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
className="mr-2"
|
||||
onCheckedChange={() => toggle(n.networkId)}
|
||||
/>
|
||||
<span>{n.name}</span>
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{n.driver}
|
||||
</Badge>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto size-4",
|
||||
isSelected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{selectedNetworks.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selectedNetworks.map((n) => (
|
||||
<Badge key={n.networkId} variant="secondary">
|
||||
{n.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,355 @@
|
||||
import { Check, ChevronsUpDown, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type ServiceType =
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "libsql";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: ServiceType;
|
||||
}
|
||||
|
||||
export const AssignNetworks = ({ id, type }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [detached, setDetached] = useState(false);
|
||||
|
||||
const {
|
||||
service,
|
||||
serverId,
|
||||
networkIds,
|
||||
detachDokployNetwork,
|
||||
updateAsync,
|
||||
isUpdating,
|
||||
refetch,
|
||||
} = useServiceNetworks(id, type);
|
||||
|
||||
const { data: networks } = api.network.all.useQuery(
|
||||
{ serverId: serverId ?? undefined },
|
||||
{ enabled: service !== undefined },
|
||||
);
|
||||
|
||||
const { data: applicationDomains } = api.domain.byApplicationId.useQuery(
|
||||
{ applicationId: id },
|
||||
{ enabled: type === "application" },
|
||||
);
|
||||
const hasDomains = (applicationDomains?.length ?? 0) > 0;
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(networkIds ?? []);
|
||||
setDetached(detachDokployNetwork ?? false);
|
||||
}, [networkIds, detachDokployNetwork]);
|
||||
|
||||
const availableNetworks = (networks ?? []).filter(
|
||||
(n) => n.driver === "overlay",
|
||||
);
|
||||
const selectedNetworks = availableNetworks.filter((n) =>
|
||||
selected.includes(n.networkId),
|
||||
);
|
||||
const isDirty =
|
||||
selected.length !== (networkIds?.length ?? 0) ||
|
||||
selected.some((networkId) => !networkIds?.includes(networkId)) ||
|
||||
detached !== detachDokployNetwork;
|
||||
|
||||
const toggle = (networkId: string) => {
|
||||
setSelected((prev) =>
|
||||
prev.includes(networkId)
|
||||
? prev.filter((id) => id !== networkId)
|
||||
: [...prev, networkId],
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
try {
|
||||
await updateAsync({
|
||||
networkIds: selected,
|
||||
detachDokployNetwork: detached,
|
||||
});
|
||||
toast.success("Networks updated. Redeploy the service to apply them.");
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
toast.error("Error updating networks", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">Networks</CardTitle>
|
||||
<CardDescription>
|
||||
Attach additional Docker networks to this service so it can reach
|
||||
services on those networks. Takes effect on the next deploy.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-row items-start justify-between gap-3 rounded-lg border p-4">
|
||||
<div className="space-y-1 pr-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
Detach from dokploy-network
|
||||
</span>
|
||||
<Badge variant="secondary">dokploy-network</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
By default the service joins the shared dokploy-network. Detach it
|
||||
to keep it reachable only through the networks you attach below.
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={detached} onCheckedChange={setDetached} />
|
||||
</div>
|
||||
|
||||
{detached && hasDomains && (
|
||||
<AlertBlock type="warning">
|
||||
Warning: this service has domains. Detaching it from dokploy-network
|
||||
will break Traefik routing, and its domains will stop working.
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
{detached && !hasDomains && selected.length === 0 && (
|
||||
<AlertBlock type="warning">
|
||||
This service is detached from dokploy-network but has no other
|
||||
network attached. It would be unreachable, so dokploy-network will
|
||||
be kept until you attach a network below.
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{selectedNetworks.length > 0
|
||||
? `${selectedNetworks.length} network${
|
||||
selectedNetworks.length > 1 ? "s" : ""
|
||||
} selected`
|
||||
: "Select networks..."}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search networks..." className="h-9" />
|
||||
<CommandList className="max-h-60">
|
||||
{availableNetworks.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
No overlay networks on this server.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CommandEmpty className="py-6 text-center text-sm text-muted-foreground">
|
||||
No networks found.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableNetworks.map((n) => {
|
||||
const isSelected = selected.includes(n.networkId);
|
||||
return (
|
||||
<CommandItem
|
||||
key={n.networkId}
|
||||
value={n.name}
|
||||
onSelect={() => toggle(n.networkId)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
className="mr-2"
|
||||
onCheckedChange={() => toggle(n.networkId)}
|
||||
/>
|
||||
<span>{n.name}</span>
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{n.driver}
|
||||
</Badge>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto size-4",
|
||||
isSelected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{selectedNetworks.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selectedNetworks.map((n) => (
|
||||
<Badge
|
||||
key={n.networkId}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 pr-1"
|
||||
>
|
||||
{n.name}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggle(n.networkId)}
|
||||
className="ml-0.5 rounded-full outline-hidden hover:opacity-70"
|
||||
>
|
||||
<X className="size-3" />
|
||||
<span className="sr-only">Remove {n.name}</span>
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button disabled={!isDirty} isLoading={isUpdating} onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Maps a service type to its one-query and update-mutation, normalizing the
|
||||
// per-type id field name and the networkIds field.
|
||||
const useServiceNetworks = (id: string, type: ServiceType) => {
|
||||
const application = api.application.one.useQuery(
|
||||
{ applicationId: id },
|
||||
{ enabled: type === "application" },
|
||||
);
|
||||
const postgres = api.postgres.one.useQuery(
|
||||
{ postgresId: id },
|
||||
{ enabled: type === "postgres" },
|
||||
);
|
||||
const mysql = api.mysql.one.useQuery(
|
||||
{ mysqlId: id },
|
||||
{ enabled: type === "mysql" },
|
||||
);
|
||||
const mariadb = api.mariadb.one.useQuery(
|
||||
{ mariadbId: id },
|
||||
{ enabled: type === "mariadb" },
|
||||
);
|
||||
const mongo = api.mongo.one.useQuery(
|
||||
{ mongoId: id },
|
||||
{ enabled: type === "mongo" },
|
||||
);
|
||||
const redis = api.redis.one.useQuery(
|
||||
{ redisId: id },
|
||||
{ enabled: type === "redis" },
|
||||
);
|
||||
const libsql = api.libsql.one.useQuery(
|
||||
{ libsqlId: id },
|
||||
{ enabled: type === "libsql" },
|
||||
);
|
||||
const applicationUpdate = api.application.update.useMutation();
|
||||
const postgresUpdate = api.postgres.update.useMutation();
|
||||
const mysqlUpdate = api.mysql.update.useMutation();
|
||||
const mariadbUpdate = api.mariadb.update.useMutation();
|
||||
const mongoUpdate = api.mongo.update.useMutation();
|
||||
const redisUpdate = api.redis.update.useMutation();
|
||||
const libsqlUpdate = api.libsql.update.useMutation();
|
||||
|
||||
const map = {
|
||||
application: {
|
||||
query: application,
|
||||
mutation: applicationUpdate,
|
||||
save: (payload: SavePayload) =>
|
||||
applicationUpdate.mutateAsync({ applicationId: id, ...payload }),
|
||||
},
|
||||
postgres: {
|
||||
query: postgres,
|
||||
mutation: postgresUpdate,
|
||||
save: (payload: SavePayload) =>
|
||||
postgresUpdate.mutateAsync({ postgresId: id, ...payload }),
|
||||
},
|
||||
mysql: {
|
||||
query: mysql,
|
||||
mutation: mysqlUpdate,
|
||||
save: (payload: SavePayload) =>
|
||||
mysqlUpdate.mutateAsync({ mysqlId: id, ...payload }),
|
||||
},
|
||||
mariadb: {
|
||||
query: mariadb,
|
||||
mutation: mariadbUpdate,
|
||||
save: (payload: SavePayload) =>
|
||||
mariadbUpdate.mutateAsync({ mariadbId: id, ...payload }),
|
||||
},
|
||||
mongo: {
|
||||
query: mongo,
|
||||
mutation: mongoUpdate,
|
||||
save: (payload: SavePayload) =>
|
||||
mongoUpdate.mutateAsync({ mongoId: id, ...payload }),
|
||||
},
|
||||
redis: {
|
||||
query: redis,
|
||||
mutation: redisUpdate,
|
||||
save: (payload: SavePayload) =>
|
||||
redisUpdate.mutateAsync({ redisId: id, ...payload }),
|
||||
},
|
||||
libsql: {
|
||||
query: libsql,
|
||||
mutation: libsqlUpdate,
|
||||
save: (payload: SavePayload) =>
|
||||
libsqlUpdate.mutateAsync({ libsqlId: id, ...payload }),
|
||||
},
|
||||
}[type];
|
||||
|
||||
const service = map.query.data;
|
||||
|
||||
return {
|
||||
service,
|
||||
serverId: service?.serverId ?? null,
|
||||
networkIds: service?.networkIds ?? [],
|
||||
detachDokployNetwork: service?.detachDokployNetwork ?? false,
|
||||
updateAsync: map.save,
|
||||
isUpdating: map.mutation.isPending,
|
||||
refetch: map.query.refetch,
|
||||
};
|
||||
};
|
||||
|
||||
type SavePayload = {
|
||||
networkIds: string[];
|
||||
detachDokployNetwork: boolean;
|
||||
};
|
||||
@@ -0,0 +1,404 @@
|
||||
"use client";
|
||||
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { Network, Plus, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
// Only bridge and overlay can be created: "host"/"none" are Docker
|
||||
// singletons and macvlan/ipvlan need driver options we don't expose.
|
||||
const networkDriverEnum = ["bridge", "overlay"] as const;
|
||||
|
||||
const ipamConfigEntrySchema = z.object({
|
||||
subnet: z.string().optional(),
|
||||
ipRange: z.string().optional(),
|
||||
gateway: z.string().optional(),
|
||||
});
|
||||
|
||||
const networkFormSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
driver: z.enum(networkDriverEnum),
|
||||
internal: z.boolean(),
|
||||
attachable: z.boolean(),
|
||||
enableIPv4: z.boolean(),
|
||||
enableIPv6: z.boolean(),
|
||||
mtu: z
|
||||
.string()
|
||||
.refine(
|
||||
(value) =>
|
||||
value === "" ||
|
||||
(/^\d+$/.test(value) && +value >= 68 && +value <= 65535),
|
||||
{ message: "MTU must be a number between 68 and 65535" },
|
||||
),
|
||||
ipamDriver: z.string().optional(),
|
||||
ipamConfig: z.array(ipamConfigEntrySchema),
|
||||
})
|
||||
.superRefine((input, ctx) => {
|
||||
if (!input.enableIPv4 && !input.enableIPv6) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["enableIPv4"],
|
||||
message: "IPv4 or IPv6 must be enabled",
|
||||
});
|
||||
}
|
||||
for (const [index, entry] of input.ipamConfig.entries()) {
|
||||
if (!entry.subnet && (entry.gateway || entry.ipRange)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["ipamConfig", index, "subnet"],
|
||||
message: "Gateway and IP range require a subnet",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type NetworkFormValues = z.infer<typeof networkFormSchema>;
|
||||
|
||||
const defaultValues: NetworkFormValues = {
|
||||
name: "",
|
||||
driver: "bridge",
|
||||
internal: false,
|
||||
attachable: false,
|
||||
enableIPv4: true,
|
||||
enableIPv6: false,
|
||||
mtu: "",
|
||||
ipamDriver: "",
|
||||
ipamConfig: [],
|
||||
};
|
||||
|
||||
const toggleOptions = [
|
||||
{
|
||||
name: "internal",
|
||||
label: "Internal",
|
||||
description: "Containers on this network cannot reach external networks.",
|
||||
},
|
||||
{
|
||||
name: "attachable",
|
||||
label: "Attachable",
|
||||
description:
|
||||
"Allow standalone containers to attach (overlay networks only).",
|
||||
},
|
||||
{
|
||||
name: "enableIPv4",
|
||||
label: "Enable IPv4",
|
||||
description: "Enable IPv4 addressing on the network.",
|
||||
},
|
||||
{
|
||||
name: "enableIPv6",
|
||||
label: "Enable IPv6",
|
||||
description: "Enable IPv6 addressing on the network.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface HandleNetworkProps {
|
||||
/** Target server; undefined creates on the local Dokploy server */
|
||||
serverId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Docker networks are immutable, so this dialog only creates them;
|
||||
// changing a network means deleting and recreating it.
|
||||
export const HandleNetwork = ({ serverId, children }: HandleNetworkProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isPending } = api.network.create.useMutation();
|
||||
|
||||
const form = useForm<NetworkFormValues>({
|
||||
resolver: zodResolver(networkFormSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const ipamConfigFieldArray = useFieldArray({
|
||||
control: form.control,
|
||||
name: "ipamConfig",
|
||||
});
|
||||
|
||||
const onSubmit = async (data: NetworkFormValues) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
driver: data.driver,
|
||||
serverId,
|
||||
internal: data.internal,
|
||||
attachable: data.attachable,
|
||||
enableIPv4: data.enableIPv4,
|
||||
enableIPv6: data.enableIPv6,
|
||||
mtu: data.mtu ? Number(data.mtu) : undefined,
|
||||
ipam: {
|
||||
driver: data.ipamDriver || undefined,
|
||||
config: data.ipamConfig,
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Network created");
|
||||
await utils.network.all.invalidate();
|
||||
setIsOpen(false);
|
||||
form.reset(defaultValues);
|
||||
} catch (error) {
|
||||
toast.error("Error creating network", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const trigger = children ?? (
|
||||
<Button>
|
||||
<Plus className=" size-4" />
|
||||
Add network
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Network className="size-5 text-muted-foreground" />
|
||||
Add network
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new Docker network for your organization. Networks are
|
||||
immutable: to change one, delete it and create it again.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-6"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-network" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="driver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Driver</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select driver" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{networkDriverEnum.map((d) => (
|
||||
<SelectItem key={d} value={d}>
|
||||
{d}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
bridge for single-server containers; overlay for Swarm
|
||||
services.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mtu"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>MTU (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1500"
|
||||
inputMode="numeric"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
Maximum transmission unit. Leave empty to use Docker's
|
||||
default.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{toggleOptions.map((option) => (
|
||||
<FormField
|
||||
key={option.name}
|
||||
control={form.control}
|
||||
name={option.name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start justify-between gap-3 space-y-0 rounded-lg border p-4">
|
||||
<div className="space-y-1 pr-1">
|
||||
<FormLabel>{option.label}</FormLabel>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
{option.description}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-4 rounded-lg border p-4">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>IPAM</FormLabel>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
IP address management settings for this network.
|
||||
</p>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ipamDriver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-muted-foreground">
|
||||
Driver (optional)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="default" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<FormLabel className="text-muted-foreground">
|
||||
Config (subnet / gateway / IP range)
|
||||
</FormLabel>
|
||||
{ipamConfigFieldArray.fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-wrap gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ipamConfig.${index}.subnet`}
|
||||
render={({ field: f }) => (
|
||||
<FormItem className="min-w-[140px] flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...f}
|
||||
placeholder="Subnet (e.g. 172.20.0.0/16)"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ipamConfig.${index}.ipRange`}
|
||||
render={({ field: f }) => (
|
||||
<FormItem className="min-w-[120px] flex-1">
|
||||
<FormControl>
|
||||
<Input {...f} placeholder="IP range" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ipamConfig.${index}.gateway`}
|
||||
render={({ field: f }) => (
|
||||
<FormItem className="min-w-[120px] flex-1">
|
||||
<FormControl>
|
||||
<Input {...f} placeholder="Gateway" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Remove IPAM config"
|
||||
onClick={() => ipamConfigFieldArray.remove(index)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
ipamConfigFieldArray.append({
|
||||
subnet: "",
|
||||
ipRange: "",
|
||||
gateway: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
Add IPAM config
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isPending}>
|
||||
Create network
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { Eye, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
networkId: string;
|
||||
networkName: string;
|
||||
}
|
||||
|
||||
export const ShowNetworkConfig = ({ networkId, networkName }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data, isLoading, error } = api.network.inspect.useQuery(
|
||||
{ networkId },
|
||||
{ enabled: open },
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm" aria-label="View network config">
|
||||
<Eye className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Network Config</DialogTitle>
|
||||
<DialogDescription>
|
||||
docker network inspect output for "{networkName}"
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{error ? (
|
||||
<AlertBlock type="error">{error.message}</AlertBlock>
|
||||
) : isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center py-10 text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
|
||||
<code>
|
||||
<pre className="whitespace-pre-wrap wrap-break-word">
|
||||
<CodeEditor
|
||||
language="json"
|
||||
lineWrapping
|
||||
lineNumbers={false}
|
||||
readOnly
|
||||
value={JSON.stringify(data, null, 2)}
|
||||
/>
|
||||
</pre>
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,494 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
import {
|
||||
ArrowUpDown,
|
||||
Loader2,
|
||||
Network,
|
||||
RotateCcw,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleNetwork } from "@/components/dashboard/networks/handle-network";
|
||||
import { ShowNetworkConfig } from "@/components/dashboard/networks/show-network-config";
|
||||
import { SyncNetworks } from "@/components/dashboard/networks/sync-networks";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { AppRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type NetworkRow = inferRouterOutputs<AppRouter>["network"]["all"][number];
|
||||
|
||||
interface Props {
|
||||
/** Selected server; undefined shows the local Dokploy server */
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
const getIpamEntries = (row: NetworkRow) =>
|
||||
(row.ipam?.config ?? []).filter((c) => c.subnet || c.gateway || c.ipRange);
|
||||
|
||||
const SortableHeader = ({
|
||||
column,
|
||||
title,
|
||||
}: {
|
||||
column: {
|
||||
getIsSorted: () => false | "asc" | "desc";
|
||||
toggleSorting: (asc: boolean) => void;
|
||||
};
|
||||
title: string;
|
||||
}) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="-ml-3 h-8"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
{title}
|
||||
<ArrowUpDown className="ml-2 size-4" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const ShowNetworks = ({ serverId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: "createdAt", desc: true },
|
||||
]);
|
||||
const [globalFilter, setGlobalFilter] = useState("");
|
||||
const [driverFilter, setDriverFilter] = useState<string>("all");
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: networks, isLoading } = api.network.all.useQuery({ serverId });
|
||||
const { mutateAsync: removeNetwork } = api.network.remove.useMutation();
|
||||
const recreateMutation = api.network.recreate.useMutation();
|
||||
|
||||
// Same query the Sync dialog uses; "missing" tells us which records
|
||||
// no longer have a real network in Docker
|
||||
const {
|
||||
data: syncStatus,
|
||||
isFetching: isVerifying,
|
||||
refetch: refetchVerify,
|
||||
} = api.network.networksToSync.useQuery({ serverId }, { enabled: verified });
|
||||
|
||||
const missingIds = useMemo(
|
||||
() => new Set(syncStatus?.missing.map((m) => m.networkId) ?? []),
|
||||
[syncStatus],
|
||||
);
|
||||
|
||||
const onVerify = async () => {
|
||||
setVerified(true);
|
||||
const { data: result, error } = await refetchVerify();
|
||||
if (error) {
|
||||
toast.error("Error verifying networks", {
|
||||
description: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!result) return;
|
||||
if (result.missing.length === 0) {
|
||||
toast.success("All networks exist in Docker");
|
||||
} else {
|
||||
toast.warning(
|
||||
`${result.missing.length} network(s) no longer exist in Docker`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
let list = networks ?? [];
|
||||
if (driverFilter !== "all") {
|
||||
list = list.filter((n) => n.driver === driverFilter);
|
||||
}
|
||||
if (globalFilter.trim()) {
|
||||
const query = globalFilter.toLowerCase();
|
||||
list = list.filter(
|
||||
(n) =>
|
||||
n.name.toLowerCase().includes(query) ||
|
||||
(n.ipam?.config ?? []).some(
|
||||
(c) =>
|
||||
c.subnet?.toLowerCase().includes(query) ||
|
||||
c.gateway?.toLowerCase().includes(query) ||
|
||||
c.ipRange?.toLowerCase().includes(query),
|
||||
),
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [networks, driverFilter, globalFilter]);
|
||||
|
||||
const columns = useMemo<ColumnDef<NetworkRow>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => <SortableHeader column={column} title="Name" />,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{row.original.name}
|
||||
{verified &&
|
||||
syncStatus &&
|
||||
(missingIds.has(row.original.networkId) ? (
|
||||
<>
|
||||
<Badge variant="red">Missing in Docker</Badge>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
isLoading={recreateMutation.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await recreateMutation.mutateAsync({
|
||||
networkId: row.original.networkId,
|
||||
});
|
||||
toast.success(
|
||||
`Network "${row.original.name}" recreated in Docker`,
|
||||
);
|
||||
await utils.network.networksToSync.invalidate();
|
||||
} catch (error) {
|
||||
toast.error("Error recreating network", {
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RotateCcw className="size-3.5" />
|
||||
Recreate
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Badge variant="green">In sync</Badge>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "driver",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Driver" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">{row.original.driver}</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{row.original.driver === "overlay" ? "swarm" : "local"}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "subnet",
|
||||
accessorFn: (row) => getIpamEntries(row)[0]?.subnet ?? "",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Subnet" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const ipamEntries = getIpamEntries(row.original);
|
||||
if (ipamEntries.length === 0) {
|
||||
return <span className="text-muted-foreground">Auto</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{ipamEntries.map((c, index) => (
|
||||
<div
|
||||
key={`${row.original.networkId}-ipam-${index}`}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<span>{c.subnet ?? "—"}</span>
|
||||
{(c.gateway || c.ipRange) && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{[
|
||||
c.gateway && `gw ${c.gateway}`,
|
||||
c.ipRange && `range ${c.ipRange}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "internal",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Internal" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground">
|
||||
{row.original.internal ? "Yes" : "No"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "attachable",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Attachable" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground">
|
||||
{row.original.attachable ? "Yes" : "No"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} title="Created" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-muted-foreground whitespace-nowrap">
|
||||
{new Date(row.original.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableSorting: false,
|
||||
header: () => <div className="text-right">Actions</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<ShowNetworkConfig
|
||||
networkId={row.original.networkId}
|
||||
networkName={row.original.name}
|
||||
/>
|
||||
<DialogAction
|
||||
title="Delete network"
|
||||
description={`The network "${row.original.name}" will be removed from Docker and Dokploy. This action cannot be undone.`}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await removeNetwork({
|
||||
networkId: row.original.networkId,
|
||||
});
|
||||
toast.success("Network deleted");
|
||||
await utils.network.all.invalidate();
|
||||
await utils.network.networksToSync.invalidate();
|
||||
} catch (error) {
|
||||
toast.error("Error deleting network", {
|
||||
description:
|
||||
error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Delete network"
|
||||
>
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[verified, syncStatus, missingIds, removeNetwork, recreateMutation, utils],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
pagination,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<Network className="size-6 text-muted-foreground self-center" />
|
||||
Networks
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage the Docker networks of the selected server.
|
||||
</CardDescription>
|
||||
<CardAction className="self-center">
|
||||
<div className="flex items-center gap-2">
|
||||
{networks && networks.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
isLoading={isVerifying}
|
||||
onClick={onVerify}
|
||||
>
|
||||
<ShieldCheck className="size-4" />
|
||||
Verify
|
||||
</Button>
|
||||
)}
|
||||
<SyncNetworks serverId={serverId} />
|
||||
{networks && networks.length > 0 && (
|
||||
<HandleNetwork serverId={serverId} />
|
||||
)}
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4 py-8 border-t">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[45vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : !networks?.length ? (
|
||||
<div className="flex min-h-[45vh] w-full flex-col items-center justify-center gap-4 rounded-lg border border-dashed p-8">
|
||||
<div className="rounded-full bg-muted p-4">
|
||||
<Network className="size-10 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-1 text-center">
|
||||
<p className="text-sm font-medium">No networks yet</p>
|
||||
<p className="max-w-sm text-sm text-muted-foreground">
|
||||
Create Docker networks for your organization and optionally
|
||||
attach them to a server. Add your first network to get
|
||||
started.
|
||||
</p>
|
||||
</div>
|
||||
<HandleNetwork serverId={serverId} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Search by name, subnet, gateway..."
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<Select value={driverFilter} onValueChange={setDriverFilter}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="Driver" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All drivers</SelectItem>
|
||||
<SelectItem value="bridge">bridge</SelectItem>
|
||||
<SelectItem value="overlay">overlay</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center text-muted-foreground"
|
||||
>
|
||||
No networks match your filters.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{table.getPageCount() > 1 && (
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {table.getState().pagination.pageIndex + 1} of{" "}
|
||||
{table.getPageCount()}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, RefreshCw, RotateCcw, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
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";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const SyncNetworks = ({ serverId }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data, isLoading, error, refetch } =
|
||||
api.network.networksToSync.useQuery({ serverId }, { enabled: open });
|
||||
|
||||
const importMutation = api.network.import.useMutation();
|
||||
const removeMutation = api.network.remove.useMutation();
|
||||
const recreateMutation = api.network.recreate.useMutation();
|
||||
|
||||
const toggleSelected = (name: string) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(name)) {
|
||||
next.delete(name);
|
||||
} else {
|
||||
next.add(name);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const onImport = async () => {
|
||||
try {
|
||||
const result = await importMutation.mutateAsync({
|
||||
serverId,
|
||||
names: Array.from(selected),
|
||||
});
|
||||
|
||||
if (result.imported.length > 0) {
|
||||
toast.success(`Imported ${result.imported.length} network(s)`);
|
||||
}
|
||||
for (const failure of result.errors) {
|
||||
toast.error(`Could not import "${failure.name}"`, {
|
||||
description: failure.error,
|
||||
});
|
||||
}
|
||||
|
||||
setSelected(new Set());
|
||||
await utils.network.all.invalidate();
|
||||
|
||||
setOpen(false);
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
toast.error("Error importing networks", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveStale = async (networkId: string, name: string) => {
|
||||
try {
|
||||
await removeMutation.mutateAsync({ networkId });
|
||||
toast.success(`Removed stale record "${name}"`);
|
||||
await utils.network.all.invalidate();
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
toast.error("Error removing record", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRecreate = async (networkId: string, name: string) => {
|
||||
try {
|
||||
await recreateMutation.mutateAsync({ networkId });
|
||||
toast.success(`Network "${name}" recreated in Docker`);
|
||||
await utils.network.all.invalidate();
|
||||
await utils.network.networksToSync.invalidate();
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
toast.error("Error recreating network", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(value) => {
|
||||
setOpen(value);
|
||||
if (!value) setSelected(new Set());
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="size-4" />
|
||||
Sync
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RefreshCw className="size-5 text-muted-foreground" />
|
||||
Sync networks
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Import networks that exist in Docker but not in Dokploy, and clean
|
||||
up records whose network no longer exists.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{error ? (
|
||||
<AlertBlock type="error">{error.message}</AlertBlock>
|
||||
) : isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center py-10 text-sm text-muted-foreground">
|
||||
<span>Scanning Docker networks...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
Found in Docker ({data?.importable.length ?? 0})
|
||||
</span>
|
||||
{data?.importable.length ? (
|
||||
data.importable.map((dockerNetwork) => (
|
||||
<label
|
||||
key={dockerNetwork.name}
|
||||
htmlFor={`import-network-${dockerNetwork.name}`}
|
||||
className="flex cursor-pointer items-center justify-between gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
id={`import-network-${dockerNetwork.name}`}
|
||||
checked={selected.has(dockerNetwork.name)}
|
||||
onCheckedChange={() =>
|
||||
toggleSelected(dockerNetwork.name)
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">
|
||||
{dockerNetwork.name}
|
||||
</span>
|
||||
{dockerNetwork.subnets.length > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{dockerNetwork.subnets.join(" · ")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">{dockerNetwork.driver}</Badge>
|
||||
</label>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Nothing to import — everything is in sync.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!!data?.missing.length && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
Missing in Docker ({data.missing.length})
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
These records exist in Dokploy but their network is gone
|
||||
from Docker.
|
||||
</span>
|
||||
{data.missing.map((stale) => (
|
||||
<div
|
||||
key={stale.networkId}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border border-dashed p-3"
|
||||
>
|
||||
<span className="text-sm">{stale.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
isLoading={recreateMutation.isPending}
|
||||
onClick={() =>
|
||||
onRecreate(stale.networkId, stale.name)
|
||||
}
|
||||
>
|
||||
<RotateCcw className="size-3.5" />
|
||||
Recreate
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label={`Remove stale record ${stale.name}`}
|
||||
isLoading={removeMutation.isPending}
|
||||
onClick={() =>
|
||||
onRemoveStale(stale.networkId, stale.name)
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={selected.size === 0}
|
||||
isLoading={importMutation.isPending}
|
||||
onClick={onImport}
|
||||
>
|
||||
Import selected ({selected.size})
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -93,7 +93,8 @@ export function AddOrganization({ organizationId }: Props) {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error(
|
||||
`Failed to ${organizationId ? "update" : "create"} organization`,
|
||||
error?.message ??
|
||||
`Failed to ${organizationId ? "update" : "create"} organization`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
@@ -99,7 +99,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Downloads and sets up the PostgreSQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -139,7 +139,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Restart the PostgreSQL service without rebuilding
|
||||
</p>
|
||||
@@ -181,7 +181,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Start the PostgreSQL database (requires a previous
|
||||
successful setup)
|
||||
@@ -221,7 +221,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>
|
||||
Stop the currently running PostgreSQL database
|
||||
</p>
|
||||
@@ -234,6 +234,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.postgresId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
@@ -248,7 +249,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<p>Open a terminal to the PostgreSQL container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
+3
-3
@@ -25,11 +25,11 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
<Input enableCopyButton disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Database Name</Label>
|
||||
<Input disabled value={data?.databaseName} />
|
||||
<Input enableCopyButton disabled value={data?.databaseName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
@@ -57,7 +57,7 @@ export const ShowInternalPostgresCredentials = ({ postgresId }: Props) => {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input disabled value={data?.appName} />
|
||||
<Input enableCopyButton disabled value={data?.appName} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -180,7 +180,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
className="z-999 w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
|
||||
@@ -191,7 +191,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
className="z-999 w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
|
||||
@@ -62,7 +62,7 @@ const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||
mariadb: "mariadb:11",
|
||||
mysql: "mysql:8",
|
||||
postgres: "postgres:18",
|
||||
redis: "redis:7",
|
||||
redis: "redis:8",
|
||||
};
|
||||
|
||||
const databasesUserDefaultPlaceholder: Record<
|
||||
@@ -412,7 +412,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
/>
|
||||
<Label
|
||||
htmlFor={key}
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
>
|
||||
{value.icon}
|
||||
{value.label}
|
||||
@@ -765,7 +765,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
name="replicaSets"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Use Replica Sets</FormLabel>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +231,7 @@ export const AddImport = ({ environmentId, projectName }: Props) => {
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
className="z-999 w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
@@ -386,7 +386,7 @@ export const AddImport = ({ environmentId, projectName }: Props) => {
|
||||
{templateInfo.template.domains.map((domain, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm"
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-xs"
|
||||
>
|
||||
<div className="font-medium">
|
||||
{domain.serviceName}
|
||||
|
||||
@@ -236,7 +236,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full sm:w-[200px] justify-between !bg-input",
|
||||
"w-full sm:w-[200px] justify-between bg-input!",
|
||||
)}
|
||||
>
|
||||
{isLoadingTags
|
||||
@@ -296,7 +296,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
variant={showBookmarksOnly ? "default" : "outline"}
|
||||
size="icon"
|
||||
onClick={() => setShowBookmarksOnly(!showBookmarksOnly)}
|
||||
className="h-9 w-9 flex-shrink-0"
|
||||
className="h-9 w-9 shrink-0"
|
||||
disabled={isLoadingBookmarks}
|
||||
>
|
||||
<Bookmark
|
||||
@@ -311,7 +311,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
onClick={() =>
|
||||
setViewMode(viewMode === "detailed" ? "icon" : "detailed")
|
||||
}
|
||||
className="h-9 w-9 flex-shrink-0"
|
||||
className="h-9 w-9 shrink-0"
|
||||
>
|
||||
{viewMode === "detailed" ? (
|
||||
<LayoutGrid className="size-4" />
|
||||
@@ -398,7 +398,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background"
|
||||
className="h-8 w-8 bg-background/80 backdrop-blur-xs hover:bg-background"
|
||||
onClick={(e) => handleToggleBookmark(e, template.id)}
|
||||
>
|
||||
<Bookmark
|
||||
@@ -451,7 +451,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
|
||||
{/* Template Content */}
|
||||
{viewMode === "detailed" && (
|
||||
<ScrollArea className="flex-1 p-6">
|
||||
<ScrollArea className="min-h-0 flex-1 p-6">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{template?.description}
|
||||
</div>
|
||||
@@ -534,7 +534,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
className="z-999 w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
|
||||
@@ -297,7 +297,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -306,7 +306,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
placeholder="Environment name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { StepProps } from "./step-two";
|
||||
export const StepThree = ({ templateInfo }: StepProps) => {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-grow">
|
||||
<div className="grow">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold">Step 3: Review and Finalize</h2>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -201,7 +201,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-6">
|
||||
<div className="flex-grow overflow-auto pb-8">
|
||||
<div className="grow overflow-auto pb-8">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
|
||||
{!selectedVariant && (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user