mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 02:16:43 +00:00
feat(v5): add WireGuard and coold/corrosion config to clusters/servers
Add WireGuard networking fields (interface, management pool, listen port), container network pool, coold/corrosion versioning, and builder CPU quota to clusters and servers via new migrations and model fillables. Expose full cluster and server CRUD on the Clusters page with private key selection, pending state tracking, and new form primitives (Field, Input, Textarea). Add server status check fields and a lima test VM config for development.
This commit is contained in:
1 parent
5d57b131ff
commit
3ae87334bd
30 files changed
+3876
-103
No files matched your search
@@ -105,6 +105,14 @@ body {
|
||||
@apply w-full min-h-full bg-gray-50 dark:bg-base dark:text-neutral-400;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;
|
||||
}
|
||||
|
||||
@@ -152,6 +152,14 @@
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
min-height: 100vh;
|
||||
|
||||
+1290
-58
File diff suppressed because it is too large.
Load diff
@@ -19,6 +19,7 @@ const buttonVariants = cva(
|
||||
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
||||
destructive:
|
||||
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
||||
delete: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
|
||||
import { XIcon } from '@phosphor-icons/react';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
@@ -21,18 +23,37 @@ function DialogOverlay({ className, ...props }: React.ComponentProps<typeof Dial
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Popup>) {
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Popup> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
'fixed left-1/2 top-1/2 w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-card p-6 shadow-lg outline-none',
|
||||
'fixed left-1/2 top-1/2 z-50 max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border border-border bg-card p-6 shadow-lg outline-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
aria-label="Close dialog"
|
||||
render={<Button type="button" variant="ghost" size="icon-sm" className="absolute top-3 right-3" />}
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Popup>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type FieldProps = React.ComponentProps<'label'>;
|
||||
|
||||
function Field({ className, ...props }: FieldProps) {
|
||||
return <label data-slot="field" className={cn('flex flex-col gap-1 text-sm', className)} {...props} />;
|
||||
}
|
||||
|
||||
type FieldLabelProps = React.ComponentProps<'span'>;
|
||||
|
||||
function FieldLabel({ className, ...props }: FieldLabelProps) {
|
||||
return <span data-slot="field-label" className={cn('font-medium text-foreground', className)} {...props} />;
|
||||
}
|
||||
|
||||
type FieldErrorProps = React.ComponentProps<'span'> & {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function FieldError({ className, message, ...props }: FieldErrorProps) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden={message ? undefined : true}
|
||||
aria-live="polite"
|
||||
data-slot="field-error"
|
||||
className={cn('min-h-4 text-xs leading-4 text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{message ?? ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export { Field, FieldError, FieldLabel };
|
||||
@@ -0,0 +1,18 @@
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Input({ className, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'rounded-md border border-border bg-background px-3 py-2 text-sm outline-none transition focus:border-ring focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,18 @@
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'min-h-24 rounded-md border border-border bg-background px-3 py-2 text-sm outline-none transition focus:border-ring focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export function usePendingIds<T extends string | number>() {
|
||||
const [pendingIds, setPendingIds] = useState<Set<T>>(() => new Set());
|
||||
|
||||
const start = useCallback((id: T): void => {
|
||||
setPendingIds((currentIds) => new Set(currentIds).add(id));
|
||||
}, []);
|
||||
|
||||
const finish = useCallback((id: T): void => {
|
||||
setPendingIds((currentIds) => {
|
||||
const nextIds = new Set(currentIds);
|
||||
nextIds.delete(id);
|
||||
|
||||
return nextIds;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const has = useCallback((id: T): boolean => pendingIds.has(id), [pendingIds]);
|
||||
|
||||
return {
|
||||
pendingIds,
|
||||
has,
|
||||
hasAny: pendingIds.size > 0,
|
||||
start,
|
||||
finish,
|
||||
};
|
||||
}
|
||||
@@ -9,20 +9,48 @@ export type V5Server = {
|
||||
id: string;
|
||||
name: string;
|
||||
host: string;
|
||||
sshUser: string;
|
||||
sshPort: number;
|
||||
status: string;
|
||||
capabilities: string[];
|
||||
builderEnabled: boolean;
|
||||
builderCapacity: number;
|
||||
builderCpuQuota: string;
|
||||
nodeAddress: string | null;
|
||||
wireguardListenPortOverride: number | null;
|
||||
wireguardEndpointOverride: string | null;
|
||||
wireguardManagementIp: string | null;
|
||||
wireguardPublicKey: string | null;
|
||||
containerSubnets: Record<string, string> | string[];
|
||||
privateKeyName: string | null;
|
||||
lastBootstrappedAt: string | null;
|
||||
lastStatusCheck: string | null;
|
||||
lastStatusOutput: string | null;
|
||||
lastStatusCheckedAt: string | null;
|
||||
};
|
||||
|
||||
export type V5Cluster = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
wireguardInterface: string;
|
||||
wireguardManagementPool: string;
|
||||
wireguardListenPort: number;
|
||||
containerNetworkPool: string;
|
||||
containerNetworkPrefix: number;
|
||||
namespaces: string[];
|
||||
defaultDenyContainers: boolean;
|
||||
cooldVersion: string;
|
||||
corrosionVersion: string;
|
||||
corrosionGossipPort: number;
|
||||
corrosionApiPort: number;
|
||||
builderEnabled: boolean;
|
||||
builderCapacity: number;
|
||||
builderCpuQuota: string;
|
||||
builderMemoryMax: string;
|
||||
builderTimeoutSecs: number;
|
||||
lastCliAction: string | null;
|
||||
lastCliStatus: string | null;
|
||||
lastCliSummary: string | null;
|
||||
lastCliRanAt: string | null;
|
||||
serversCount: number;
|
||||
servers: V5Server[];
|
||||
};
|
||||
@@ -38,9 +66,15 @@ export type V5Project = {
|
||||
environments: V5Environment[];
|
||||
};
|
||||
|
||||
export type V5PrivateKey = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type V5DashboardProps = {
|
||||
flux: FluxStatus | null;
|
||||
clusters?: V5Cluster[];
|
||||
privateKeys?: V5PrivateKey[];
|
||||
projects?: V5Project[];
|
||||
selectedProjectUuid?: string | null;
|
||||
selectedEnvironmentUuid?: string | null;
|
||||
|
||||
Reference in new issue
Block a user