Compare commits

..

No commits in common. "9d0e62f80ee83794b61bd389413eef5c49af86c5" and "fe7450d6ec1f7862ddc2991f5f7b2dac5fd2ba4f" have entirely different histories.

7 changed files with 72 additions and 222 deletions

View File

@ -407,10 +407,6 @@ func (f *FieldConfig) validateSingleValue(val string, index int) error {
if _, err := url.Parse(val); err != nil { if _, err := url.Parse(val); err != nil {
return fmt.Errorf("%s is not a valid URL: %s", fieldRef, val) return fmt.Errorf("%s is not a valid URL: %s", fieldRef, val)
} }
case "cidr":
if _, _, err := net.ParseCIDR(val); err != nil {
return fmt.Errorf("%s is not a valid CIDR notation: %s", fieldRef, val)
}
default: default:
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType) return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
} }

View File

@ -28,7 +28,8 @@ type IPv4StaticConfig struct {
} }
type IPv6StaticConfig struct { type IPv6StaticConfig struct {
Address null.String `json:"address,omitempty" validate_type:"cidr" required:"true"` Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"`
Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"`
Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"` Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"`
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"` DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
} }

View File

@ -33,54 +33,56 @@ export default function Ipv6NetworkCard({
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && ( {networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold">IPv6 Addresses</h4> <h4 className="text-sm font-semibold">IPv6 Addresses</h4>
{networkState.ipv6_addresses.map(addr => ( {networkState.ipv6_addresses.map(
<div addr => (
key={addr.address} <div
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent" key={addr.address}
> className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
<div className="grid grid-cols-2 gap-x-8 gap-y-4"> >
<div className="col-span-2 flex flex-col justify-between"> <div className="grid grid-cols-2 gap-x-8 gap-y-4">
<span className="text-sm text-slate-600 dark:text-slate-400"> <div className="col-span-2 flex flex-col justify-between">
Address <span className="text-sm text-slate-600 dark:text-slate-400">
</span> Address
<span className="text-sm font-medium">{addr.address}</span> </span>
</div> <span className="text-sm font-medium">{addr.address}</span>
</div>
{addr.valid_lifetime && ( {addr.valid_lifetime && (
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Valid Lifetime Valid Lifetime
</span> </span>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{addr.valid_lifetime === "" ? ( {addr.valid_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
N/A N/A
</span> </span>
) : ( ) : (
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} /> <LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
)} )}
</span> </span>
</div> </div>
)} )}
{addr.preferred_lifetime && ( {addr.preferred_lifetime && (
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Preferred Lifetime Preferred Lifetime
</span> </span>
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{addr.preferred_lifetime === "" ? ( {addr.preferred_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
N/A N/A
</span> </span>
) : ( ) : (
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} /> <LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
)} )}
</span> </span>
</div> </div>
)} )}
</div>
</div> </div>
</div> ),
))} )}
</div> </div>
)} )}
</div> </div>

View File

@ -1,6 +1,6 @@
import { LuPlus, LuX } from "react-icons/lu"; import { LuPlus, LuX } from "react-icons/lu";
import { useFieldArray, useFormContext } from "react-hook-form"; import { useFieldArray, useFormContext } from "react-hook-form";
import { useEffect } from "react"; import validator from "validator";
import { GridCard } from "@/components/Card"; import { GridCard } from "@/components/Card";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
@ -8,15 +8,10 @@ import { InputFieldWithLabel } from "@/components/InputField";
export default function StaticIpv4Card() { export default function StaticIpv4Card() {
const formMethods = useFormContext(); const formMethods = useFormContext();
const { register, formState, watch } = formMethods; const { register, formState } = formMethods;
const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" }); 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 ( return (
<GridCard> <GridCard>
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white"> <div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
@ -64,11 +59,11 @@ export default function StaticIpv4Card() {
size="SM" size="SM"
placeholder="1.1.1.1" placeholder="1.1.1.1"
{...register(`ipv4_static.dns.${index}`, { {...register(`ipv4_static.dns.${index}`, {
// validate: (value: string) => { validate: (value: string) => {
// if (value === "") return true; if (value === "") return true;
// if (!validator.isIP(value)) return "Invalid IP address"; if (!validator.isIP(value)) return "Invalid IP address";
// return true; return true;
// }, },
})} })}
error={formState.errors.ipv4_static?.dns?.[index]?.message} error={formState.errors.ipv4_static?.dns?.[index]?.message}
/> />
@ -97,7 +92,6 @@ export default function StaticIpv4Card() {
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
type="button" type="button"
text="Add DNS Server" text="Add DNS Server"
disabled={dns[0] === ""}
/> />
</div> </div>
</div> </div>

View File

@ -1,113 +0,0 @@
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 (
<GridCard>
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
Static IPv6 Configuration
</h3>
<InputFieldWithLabel
label="IP Address/Prefix"
type="text"
size="SM"
placeholder="2001:db8::1/64"
{...register("ipv6_static.address", {
validate: (value: string) => {
if (value === "") return true;
// Check if it's a valid IPv6 address with CIDR notation
const parts = value.split("/");
if (parts.length !== 2)
return "Please use CIDR notation (e.g., 2001:db8::1/64)";
const [address, prefix] = parts;
if (!validator.isIP(address, 6)) return "Invalid IPv6 address";
const prefixNum = parseInt(prefix);
if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) {
return "Prefix must be between 0 and 128";
}
return true;
},
})}
error={formState.errors.ipv6_static?.address?.message}
/>
<InputFieldWithLabel
label="Gateway"
type="text"
size="SM"
placeholder="2001:db8::1"
{...register("ipv6_static.gateway")}
/>
{/* DNS server fields */}
<div className="space-y-4">
{fields.map((dns, index) => {
return (
<div key={dns.id}>
<div className="flex items-start gap-x-2">
<div className="flex-1">
<InputFieldWithLabel
label={index === 0 ? "DNS Server" : null}
type="text"
size="SM"
placeholder="2001:4860:4860::8888"
{...register(`ipv6_static.dns.${index}`, {
validate: (value: string) => {
if (value === "") return true;
if (!validator.isIP(value)) return "Invalid IP address";
return true;
},
})}
error={formState.errors.ipv6_static?.dns?.[index]?.message}
/>
</div>
{index > 0 && (
<div className="flex-shrink-0">
<Button
size="SM"
theme="light"
type="button"
onClick={() => remove(index)}
LeadingIcon={LuX}
/>
</div>
)}
</div>
</div>
);
})}
</div>
<Button
size="SM"
theme="light"
onClick={() => append("", { shouldFocus: true })}
LeadingIcon={LuPlus}
type="button"
text="Add DNS Server"
disabled={dns[0] === ""}
/>
</div>
</div>
</GridCard>
);
}

View File

@ -752,13 +752,6 @@ export interface IPv4StaticConfig {
dns: string[]; dns: string[];
} }
export interface IPv6StaticConfig {
address: string;
prefix: string;
gateway: string;
dns: string[];
}
export interface NetworkSettings { export interface NetworkSettings {
hostname: string | null; hostname: string | null;
domain: string | null; domain: string | null;
@ -766,7 +759,6 @@ export interface NetworkSettings {
ipv4_mode: IPv4Mode; ipv4_mode: IPv4Mode;
ipv4_static?: IPv4StaticConfig; ipv4_static?: IPv4StaticConfig;
ipv6_mode: IPv6Mode; ipv6_mode: IPv6Mode;
ipv6_static?: IPv6StaticConfig;
lldp_mode: LLDPMode; lldp_mode: LLDPMode;
lldp_tx_tlvs: string[]; lldp_tx_tlvs: string[];
mdns_mode: mDNSMode; mdns_mode: mDNSMode;

View File

@ -21,7 +21,6 @@ import AutoHeight from "../components/AutoHeight";
import DhcpLeaseCard from "../components/DhcpLeaseCard"; import DhcpLeaseCard from "../components/DhcpLeaseCard";
import StaticIpv4Card from "../components/StaticIpv4Card"; import StaticIpv4Card from "../components/StaticIpv4Card";
import { useJsonRpc } from "../hooks/useJsonRpc"; import { useJsonRpc } from "../hooks/useJsonRpc";
import StaticIpv6Card from "../components/StaticIpv6Card";
import { SettingsItem } from "./devices.$id.settings"; import { SettingsItem } from "./devices.$id.settings";
@ -107,13 +106,6 @@ export default function SettingsNetworkRoute() {
gateway: settings.ipv4_static?.gateway || state.dhcp_lease?.routers?.[0] || "", gateway: settings.ipv4_static?.gateway || state.dhcp_lease?.routers?.[0] || "",
dns: settings.ipv4_static?.dns || state.dhcp_lease?.dns_servers || [], 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 }; return { settings: settingsWithDefaults, state };
@ -151,12 +143,6 @@ export default function SettingsNetworkRoute() {
// Remove empty DNS entries // Remove empty DNS entries
dns: data.ipv4_static?.dns.filter((dns: string) => dns.trim() !== ""), 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 => { send("setNetworkSettings", { settings }, async resp => {
@ -174,8 +160,7 @@ export default function SettingsNetworkRoute() {
}); });
}; };
const ipv4mode = watch("ipv4_mode"); const isIPv4Mode = watch("ipv4_mode");
const ipv6mode = watch("ipv6_mode");
return ( return (
<> <>
<FormProvider {...formMethods}> <FormProvider {...formMethods}>
@ -324,9 +309,9 @@ export default function SettingsNetworkRoute() {
</div> </div>
</div> </div>
</GridCard> </GridCard>
) : ipv4mode === "static" ? ( ) : isIPv4Mode === "static" ? (
<StaticIpv4Card /> <StaticIpv4Card />
) : ipv4mode === "dhcp" ? ( ) : isIPv4Mode === "dhcp" ? (
<DhcpLeaseCard <DhcpLeaseCard
networkState={networkState} networkState={networkState}
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm} setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
@ -344,10 +329,7 @@ export default function SettingsNetworkRoute() {
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode"> <SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
options={[ options={[{ value: "slaac", label: "SLAAC" }]}
{ value: "slaac", label: "SLAAC" },
{ value: "static", label: "Static" },
]}
{...register("ipv6_mode")} {...register("ipv6_mode")}
/> />
</SettingsItem> </SettingsItem>
@ -368,27 +350,23 @@ export default function SettingsNetworkRoute() {
</div> </div>
</div> </div>
</GridCard> </GridCard>
) : ipv6mode === "static" ? (
<StaticIpv6Card />
) : ( ) : (
<Ipv6NetworkCard networkState={networkState || undefined} /> <Ipv6NetworkCard networkState={networkState || undefined} />
)} )}
</AutoHeight> </AutoHeight>
</div> </div>
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
{(formState.isDirty || formState.isSubmitting) && ( {(formState.isDirty || formState.isSubmitting) && (
<> <div className="animate-fadeInStill opacity-0 animation-duration-300">
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" /> <Button
<div className="animate-fadeInStill opacity-0 animation-duration-300"> size="SM"
<Button theme="primary"
size="SM" disabled={formState.isSubmitting}
theme="primary" loading={formState.isSubmitting}
disabled={formState.isSubmitting} type="submit"
loading={formState.isSubmitting} text={formState.isSubmitting ? "Saving..." : "Save Settings"}
type="submit" />
text={formState.isSubmitting ? "Saving..." : "Save Settings"} </div>
/>
</div>
</>
)} )}
</div> </div>
</form> </form>