diff --git a/internal/network/config.go b/internal/network/config.go index 8a28d51..18f7b13 100644 --- a/internal/network/config.go +++ b/internal/network/config.go @@ -29,7 +29,7 @@ type IPv4StaticConfig struct { type IPv6StaticConfig struct { Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"` - Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"` + Prefix null.String `json:"prefix,omitempty" required:"true"` Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"` DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"` } diff --git a/ui/src/components/Ipv6NetworkCard.tsx b/ui/src/components/Ipv6NetworkCard.tsx index fabb7b6..ac9d20f 100644 --- a/ui/src/components/Ipv6NetworkCard.tsx +++ b/ui/src/components/Ipv6NetworkCard.tsx @@ -33,56 +33,54 @@ export default function Ipv6NetworkCard({ {networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (

IPv6 Addresses

- {networkState.ipv6_addresses.map( - addr => ( -
-
-
- - Address - - {addr.address} -
- - {addr.valid_lifetime && ( -
- - Valid Lifetime - - - {addr.valid_lifetime === "" ? ( - - N/A - - ) : ( - - )} - -
- )} - {addr.preferred_lifetime && ( -
- - Preferred Lifetime - - - {addr.preferred_lifetime === "" ? ( - - N/A - - ) : ( - - )} - -
- )} + {networkState.ipv6_addresses.map(addr => ( +
+
+
+ + Address + + {addr.address}
+ + {addr.valid_lifetime && ( +
+ + Valid Lifetime + + + {addr.valid_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )} + {addr.preferred_lifetime && ( +
+ + Preferred Lifetime + + + {addr.preferred_lifetime === "" ? ( + + N/A + + ) : ( + + )} + +
+ )}
- ), - )} +
+ ))}
)}
diff --git a/ui/src/components/StaticIpv4Card.tsx b/ui/src/components/StaticIpv4Card.tsx index f7913c9..044d2bf 100644 --- a/ui/src/components/StaticIpv4Card.tsx +++ b/ui/src/components/StaticIpv4Card.tsx @@ -1,6 +1,6 @@ import { LuPlus, LuX } from "react-icons/lu"; import { useFieldArray, useFormContext } from "react-hook-form"; -import validator from "validator"; +import { useEffect } from "react"; import { GridCard } from "@/components/Card"; import { Button } from "@/components/Button"; @@ -8,10 +8,15 @@ import { InputFieldWithLabel } from "@/components/InputField"; export default function StaticIpv4Card() { const formMethods = useFormContext(); - const { register, formState } = formMethods; + const { register, formState, watch } = formMethods; const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" }); + useEffect(() => { + if (fields.length === 0) append(""); + }, [append, fields.length]); + + const dns = watch("ipv4_static.dns"); return (
@@ -59,11 +64,11 @@ export default function StaticIpv4Card() { size="SM" placeholder="1.1.1.1" {...register(`ipv4_static.dns.${index}`, { - validate: (value: string) => { - if (value === "") return true; - if (!validator.isIP(value)) return "Invalid IP address"; - return true; - }, + // validate: (value: string) => { + // if (value === "") return true; + // if (!validator.isIP(value)) return "Invalid IP address"; + // return true; + // }, })} error={formState.errors.ipv4_static?.dns?.[index]?.message} /> @@ -92,6 +97,7 @@ export default function StaticIpv4Card() { LeadingIcon={LuPlus} type="button" text="Add DNS Server" + disabled={dns[0] === ""} />
diff --git a/ui/src/components/StaticIpv6Card.tsx b/ui/src/components/StaticIpv6Card.tsx new file mode 100644 index 0000000..845f2ed --- /dev/null +++ b/ui/src/components/StaticIpv6Card.tsx @@ -0,0 +1,107 @@ +import { LuPlus, LuX } from "react-icons/lu"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import validator from "validator"; +import { useEffect } from "react"; + +import { GridCard } from "@/components/Card"; +import { Button } from "@/components/Button"; +import { InputFieldWithLabel } from "@/components/InputField"; + +export default function StaticIpv6Card() { + const formMethods = useFormContext(); + const { register, formState, watch } = formMethods; + + const { fields, append, remove } = useFieldArray({ name: "ipv6_static.dns" }); + + useEffect(() => { + if (fields.length === 0) append(""); + }, [append, fields.length]); + + const dns = watch("ipv6_static.dns"); + return ( + +
+
+

+ Static IPv6 Configuration +

+ +
+ + + +
+ + + + {/* DNS server fields */} +
+ {fields.map((dns, index) => { + return ( +
+
+
+ { + if (value === "") return true; + if (!validator.isIP(value)) return "Invalid IP address"; + return true; + }, + })} + error={formState.errors.ipv6_static?.dns?.[index]?.message} + /> +
+ {index > 0 && ( +
+
+ )} +
+
+ ); + })} +
+ +
+
+
+ ); +} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index e75fab4..27eca66 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -752,6 +752,13 @@ export interface IPv4StaticConfig { dns: string[]; } +export interface IPv6StaticConfig { + address: string; + prefix: string; + gateway: string; + dns: string[]; +} + export interface NetworkSettings { hostname: string | null; domain: string | null; @@ -759,6 +766,7 @@ export interface NetworkSettings { ipv4_mode: IPv4Mode; ipv4_static?: IPv4StaticConfig; ipv6_mode: IPv6Mode; + ipv6_static?: IPv6StaticConfig; lldp_mode: LLDPMode; lldp_tx_tlvs: string[]; mdns_mode: mDNSMode; diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index a80d4d6..6781cf0 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -21,6 +21,7 @@ import AutoHeight from "../components/AutoHeight"; import DhcpLeaseCard from "../components/DhcpLeaseCard"; import StaticIpv4Card from "../components/StaticIpv4Card"; import { useJsonRpc } from "../hooks/useJsonRpc"; +import StaticIpv6Card from "../components/StaticIpv6Card"; import { SettingsItem } from "./devices.$id.settings"; @@ -106,6 +107,13 @@ export default function SettingsNetworkRoute() { gateway: settings.ipv4_static?.gateway || state.dhcp_lease?.routers?.[0] || "", dns: settings.ipv4_static?.dns || state.dhcp_lease?.dns_servers || [], }, + ipv6_static: { + address: + settings.ipv6_static?.address || state.ipv6_addresses?.[0]?.address || "", + prefix: settings.ipv6_static?.prefix || state.ipv6_addresses?.[0]?.prefix || "", + gateway: settings.ipv6_static?.gateway || "", + dns: settings.ipv6_static?.dns || [], + }, }; return { settings: settingsWithDefaults, state }; @@ -143,6 +151,12 @@ export default function SettingsNetworkRoute() { // Remove empty DNS entries dns: data.ipv4_static?.dns.filter((dns: string) => dns.trim() !== ""), }, + ipv6_static: { + ...data.ipv6_static, + + // Remove empty DNS entries + dns: data.ipv6_static?.dns.filter((dns: string) => dns.trim() !== ""), + }, }; send("setNetworkSettings", { settings }, async resp => { @@ -160,7 +174,8 @@ export default function SettingsNetworkRoute() { }); }; - const isIPv4Mode = watch("ipv4_mode"); + const ipv4mode = watch("ipv4_mode"); + const ipv6mode = watch("ipv6_mode"); return ( <> @@ -309,9 +324,9 @@ export default function SettingsNetworkRoute() { - ) : isIPv4Mode === "static" ? ( + ) : ipv4mode === "static" ? ( - ) : isIPv4Mode === "dhcp" ? ( + ) : ipv4mode === "dhcp" ? ( @@ -350,23 +368,27 @@ export default function SettingsNetworkRoute() { + ) : ipv6mode === "static" ? ( + ) : ( )} -
{(formState.isDirty || formState.isSubmitting) && ( -
-
+ <> +
+
+
+ )}