Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4,823 changes: 4,823 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function Header() {
}}
/>
<span className="font-bold text-lg">Dock-Dploy</span>
<span className="text-[8px] text-muted-foreground hidden sm:inline-block">by HHF Technology</span>
</div>
<div className="flex items-center gap-2">
<Button
Expand Down
50 changes: 38 additions & 12 deletions src/components/SidebarUI.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@

import { useState, useEffect } from "react";
import { ChevronDown, Container, FileText, Clock } from "lucide-react";
import { useNavigate, useRouter } from "@tanstack/react-router";
import {
Expand All @@ -10,6 +12,7 @@ import {
SidebarMenuItem,
SidebarHeader,
SidebarFooter,
useSidebar,
} from "./ui/sidebar";
import {
Collapsible,
Expand Down Expand Up @@ -48,33 +51,56 @@ export function SidebarUI() {
const navigate = useNavigate();
const router = useRouter();
const location = router.state.location;
const { toggleSidebar, state } = useSidebar();
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({});

// Initialize open groups based on current route
useEffect(() => {
const newOpenGroups = { ...openGroups };
let hasChanges = false;
Object.entries(groupedItems).forEach(([groupName, groupItems]) => {
if (groupItems.some((item) => location.pathname === item.url)) {
if (!newOpenGroups[groupName]) {
newOpenGroups[groupName] = true;
hasChanges = true;
}
}
});
if (hasChanges) {
setOpenGroups(newOpenGroups);
}
}, [location.pathname]);

return (
<>
<SidebarHeader className="border-b border-sidebar-border">
<div className="flex items-center gap-2 px-2 py-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<div className="flex items-center gap-2">
<div
onClick={toggleSidebar}
className="flex h-8 w-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground cursor-pointer hover:bg-sidebar-primary/90 transition-colors"
>
<Container className="h-4 w-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Dock-Dploy</span>
<div className="grid flex-1 text-left text-sm leading-tight group-data-[state=collapsed]:hidden">
<div className="flex items-baseline gap-1 truncate">
<span className="font-semibold">Setup Tools</span>
<span className="text-xs text-sidebar-foreground/70 truncate">v0.1.0</span>
</div>
<span className="truncate text-xs text-sidebar-foreground/70">
Setup Tools
</span>
</div>
</div>
</SidebarHeader>

<SidebarContent>
{Object.entries(groupedItems).map(([groupName, groupItems]) => {
const isGroupOpen = groupItems.some(
(item) => location.pathname === item.url
);
const isOpen = state === "collapsed" ? true : (openGroups[groupName] || false);

return (
<Collapsible
key={groupName}
defaultOpen={isGroupOpen}
open={isOpen}
onOpenChange={(open) => setOpenGroups((prev) => ({ ...prev, [groupName]: open }))}
className="group/collapsible"
>
<SidebarGroup>
Expand Down Expand Up @@ -118,9 +144,9 @@ export function SidebarUI() {
})}
</SidebarContent>

<SidebarFooter className="border-t border-sidebar-border p-2">
<div className="px-2 py-1.5 text-xs text-sidebar-foreground/70">
© {new Date().getFullYear()} Dock-Dploy
<SidebarFooter className="p-4 border-t border-border/50">
<div className="flex flex-col gap-1 text-xs text-muted-foreground group-data-[state=collapsed]:hidden">
<p>© 2025 Dock-Dploy</p>
</div>
</SidebarFooter>
</>
Expand Down
185 changes: 185 additions & 0 deletions src/components/compose-builder/NetworkForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Label } from "../../components/ui/label";
import { Input } from "../../components/ui/input";
import { Toggle } from "../../components/ui/toggle";
import type { NetworkConfig } from "../../types/compose";

interface NetworkFormProps {
network: NetworkConfig;
onUpdate: (field: keyof NetworkConfig, value: any) => void;
}

export function NetworkForm({ network, onUpdate }: NetworkFormProps) {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-2 pb-3 border-b border-border/50">
<div className="h-8 w-1 bg-primary rounded-full"></div>
<h2 className="font-bold text-lg text-foreground">Network Configuration</h2>
</div>

{/* Basic Settings */}
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-sm font-medium">Network Name</Label>
<Input
value={network.name || ""}
onChange={(e) => onUpdate("name", e.target.value)}
placeholder="e.g. frontend-network"
className="shadow-sm"
/>
</div>

<div className="space-y-2">
<Label className="text-sm font-medium">Driver</Label>
<Input
value={network.driver || ""}
onChange={(e) => onUpdate("driver", e.target.value)}
placeholder="e.g. bridge, overlay"
className="shadow-sm"
/>
<p className="text-xs text-muted-foreground">Common: bridge (default), overlay, host, none</p>
</div>
</div>

{/* Advanced Options */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-foreground/90 flex items-center gap-2">
<div className="h-4 w-0.5 bg-primary/50 rounded-full"></div>
Options
</h3>

<div className="grid grid-cols-2 gap-3">
<Toggle
pressed={!!network.attachable}
onPressedChange={(v) => onUpdate("attachable", v)}
aria-label="Attachable"
className="border rounded-md px-3 py-2 h-auto justify-center data-[state=on]:bg-primary/10 data-[state=on]:border-primary transition-all"
>
<span className="select-none text-sm">Attachable</span>
</Toggle>

<Toggle
pressed={!!network.internal}
onPressedChange={(v) => onUpdate("internal", v)}
aria-label="Internal"
className="border rounded-md px-3 py-2 h-auto justify-center data-[state=on]:bg-primary/10 data-[state=on]:border-primary transition-all"
>
<span className="select-none text-sm">Internal</span>
</Toggle>

<Toggle
pressed={!!network.enable_ipv6}
onPressedChange={(v) => onUpdate("enable_ipv6", v)}
aria-label="Enable IPv6"
className="border rounded-md px-3 py-2 h-auto justify-center data-[state=on]:bg-primary/10 data-[state=on]:border-primary transition-all"
>
<span className="select-none text-sm">IPv6</span>
</Toggle>

<Toggle
pressed={!!network.external}
onPressedChange={(v) => onUpdate("external", v)}
aria-label="External"
className="border rounded-md px-3 py-2 h-auto justify-center data-[state=on]:bg-primary/10 data-[state=on]:border-primary transition-all"
>
<span className="select-none text-sm">External</span>
</Toggle>
</div>

{network.external && (
<div className="space-y-2 pl-4 border-l-2 border-primary/30">
<Label className="text-sm font-medium">External Network Name</Label>
<Input
value={network.name_external || ""}
onChange={(e) => onUpdate("name_external", e.target.value)}
placeholder="Existing network name"
className="shadow-sm"
/>
<p className="text-xs text-muted-foreground">Reference an existing network</p>
</div>
)}
</div>

{/* IPAM Configuration */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-foreground/90 flex items-center gap-2">
<div className="h-4 w-0.5 bg-primary/50 rounded-full"></div>
IPAM (IP Address Management)
</h3>

<div className="space-y-2">
<Label className="text-sm font-medium">IPAM Driver</Label>
<Input
value={network.ipam?.driver || ""}
onChange={(e) => {
const updated = { ...network.ipam, driver: e.target.value };
onUpdate("ipam", updated);
}}
placeholder="default (leave empty for default)"
className="shadow-sm"
/>
<p className="text-xs text-muted-foreground">
Usually leave empty for default driver
</p>
</div>

<div className="space-y-3">
<Label className="text-sm font-medium">IP Configurations</Label>
{network.ipam?.config?.map((cfg, idx) => (
<div key={idx} className="flex gap-2 items-start p-3 border rounded-md bg-card/50">
<div className="flex-1 space-y-2">
<Input
value={cfg.subnet || ""}
onChange={(e) => {
const newConfig = [...(network.ipam?.config || [])];
newConfig[idx] = { ...newConfig[idx], subnet: e.target.value };
onUpdate("ipam", { ...network.ipam, config: newConfig });
}}
placeholder="Subnet (e.g. 192.168.1.0/24)"
className="shadow-sm text-sm"
/>
<Input
value={cfg.gateway || ""}
onChange={(e) => {
const newConfig = [...(network.ipam?.config || [])];
newConfig[idx] = { ...newConfig[idx], gateway: e.target.value };
onUpdate("ipam", { ...network.ipam, config: newConfig });
}}
placeholder="Gateway (e.g. 192.168.1.1)"
className="shadow-sm text-sm"
/>
</div>
<button
type="button"
onClick={() => {
const newConfig = network.ipam?.config?.filter((_, i) => i !== idx) || [];
onUpdate("ipam", { ...network.ipam, config: newConfig });
}}
className="text-destructive hover:text-destructive/80 p-1.5 rounded hover:bg-destructive/10 transition-colors"
title="Remove IP config"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
<button
type="button"
onClick={() => {
const newConfig = [...(network.ipam?.config || []), { subnet: "", gateway: "" }];
onUpdate("ipam", { ...network.ipam, config: newConfig });
}}
className="w-full py-2 px-3 border border-dashed rounded-md text-sm text-muted-foreground hover:text-foreground hover:border-primary/50 transition-colors"
>
+ Add IP Configuration
</button>
<p className="text-xs text-muted-foreground">
For ipvlan networks, define the subnet and gateway for IP allocation
</p>
</div>
</div>
</div>
);
}

Loading
Loading