From 76d256b69aa9e589e3dcf2f86ece9eb5ffacbea2 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Sat, 11 Oct 2025 16:15:35 +0000 Subject: [PATCH] feat: add CIDR notation support for IPv4 address --- ui/src/components/StaticIpv4Card.tsx | 33 +++++++++++++++---- .../routes/devices.$id.settings.network.tsx | 11 +++++++ ui/src/utils/ip.ts | 10 ++++++ 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 ui/src/utils/ip.ts diff --git a/ui/src/components/StaticIpv4Card.tsx b/ui/src/components/StaticIpv4Card.tsx index e2eaf2c5..c0c5d19c 100644 --- a/ui/src/components/StaticIpv4Card.tsx +++ b/ui/src/components/StaticIpv4Card.tsx @@ -2,31 +2,49 @@ import { LuPlus, LuX } from "react-icons/lu"; import { useFieldArray, useFormContext } from "react-hook-form"; import { useEffect } from "react"; import validator from "validator"; +import { cx } from "cva"; import { GridCard } from "@/components/Card"; import { Button } from "@/components/Button"; import { InputFieldWithLabel } from "@/components/InputField"; import { NetworkSettings } from "@/hooks/stores"; +import { netMaskFromCidr4 } from "@/utils/ip"; export default function StaticIpv4Card() { const formMethods = useFormContext(); - const { register, formState, watch } = formMethods; + const { register, formState, watch, setValue } = formMethods; const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" }); - // TODO: set subnet mask if IP address is in CIDR notation - useEffect(() => { if (fields.length === 0) append(""); }, [append, fields.length]); const dns = watch("ipv4_static.dns"); + const ipv4StaticAddress = watch("ipv4_static.address"); + const hideSubnetMask = ipv4StaticAddress?.includes("/"); + useEffect(() => { + const parts = ipv4StaticAddress?.split("/", 2); + if (parts.length !== 2) return; + + const cidrNotation = parseInt(parts[1]); + if (isNaN(cidrNotation) || cidrNotation < 0 || cidrNotation > 32) return; + + const mask = netMaskFromCidr4(cidrNotation); + setValue("ipv4_static.netmask", mask); + }, [ipv4StaticAddress, setValue]); + const validate = (value: string) => { if (!validator.isIP(value)) return "Invalid IP address"; return true; }; + const validateIsIPOrCIDR4 = (value: string) => { + if (!validator.isIP(value, 4) && !validator.isIPRange(value, 4)) return "Invalid IP address or CIDR notation"; + return true; + }; + return (
@@ -35,24 +53,25 @@ export default function StaticIpv4Card() { Static IPv4 Configuration -
+
- + />}
{ + if (settings.ipv4_static?.address?.includes("/")) { + const parts = settings.ipv4_static.address.split("/"); + const cidrNotation = parseInt(parts[1]); + if (isNaN(cidrNotation) || cidrNotation < 0 || cidrNotation > 32) { + return notifications.error("Invalid CIDR notation for IPv4 address"); + } + settings.ipv4_static.netmask = netMaskFromCidr4(cidrNotation); + settings.ipv4_static.address = parts[0]; + } + send("setNetworkSettings", { settings }, async (resp) => { if ("error" in resp) { return notifications.error( diff --git a/ui/src/utils/ip.ts b/ui/src/utils/ip.ts new file mode 100644 index 00000000..d9ad2389 --- /dev/null +++ b/ui/src/utils/ip.ts @@ -0,0 +1,10 @@ +export const netMaskFromCidr4 = (cidr: number) => { + const mask = []; + let bitCount = cidr; + for(let i=0; i<4; i++) { + const n = Math.min(bitCount, 8); + mask.push(256 - Math.pow(2, 8-n)); + bitCount -= n; + } + return mask.join('.'); +}; \ No newline at end of file