mirror of https://github.com/jetkvm/kvm.git
feat(ui): add IPv6 static configuration support in network settings
This commit is contained in:
parent
fe7450d6ec
commit
1099f56054
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -33,56 +33,54 @@ export default function Ipv6NetworkCard({
|
|||
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold">IPv6 Addresses</h4>
|
||||
{networkState.ipv6_addresses.map(
|
||||
addr => (
|
||||
<div
|
||||
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">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Address
|
||||
</span>
|
||||
<span className="text-sm font-medium">{addr.address}</span>
|
||||
</div>
|
||||
|
||||
{addr.valid_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Valid Lifetime
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.valid_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{addr.preferred_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Preferred Lifetime
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.preferred_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{networkState.ipv6_addresses.map(addr => (
|
||||
<div
|
||||
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">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Address
|
||||
</span>
|
||||
<span className="text-sm font-medium">{addr.address}</span>
|
||||
</div>
|
||||
|
||||
{addr.valid_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Valid Lifetime
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.valid_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{addr.preferred_lifetime && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Preferred Lifetime
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{addr.preferred_lifetime === "" ? (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
N/A
|
||||
</span>
|
||||
) : (
|
||||
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<GridCard>
|
||||
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||
|
@ -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] === ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<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>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InputFieldWithLabel
|
||||
label="IP Address"
|
||||
type="text"
|
||||
size="SM"
|
||||
placeholder="2001:db8::1"
|
||||
{...register("ipv6_static.address")}
|
||||
/>
|
||||
|
||||
<InputFieldWithLabel
|
||||
label="Prefix"
|
||||
type="text"
|
||||
size="SM"
|
||||
placeholder="64"
|
||||
{...register("ipv6_static.prefix")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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="1.1.1.1"
|
||||
{...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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<FormProvider {...formMethods}>
|
||||
|
@ -309,9 +324,9 @@ export default function SettingsNetworkRoute() {
|
|||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
) : isIPv4Mode === "static" ? (
|
||||
) : ipv4mode === "static" ? (
|
||||
<StaticIpv4Card />
|
||||
) : isIPv4Mode === "dhcp" ? (
|
||||
) : ipv4mode === "dhcp" ? (
|
||||
<DhcpLeaseCard
|
||||
networkState={networkState}
|
||||
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
||||
|
@ -329,7 +344,10 @@ export default function SettingsNetworkRoute() {
|
|||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
options={[{ value: "slaac", label: "SLAAC" }]}
|
||||
options={[
|
||||
{ value: "slaac", label: "SLAAC" },
|
||||
{ value: "static", label: "Static" },
|
||||
]}
|
||||
{...register("ipv6_mode")}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
@ -350,23 +368,27 @@ export default function SettingsNetworkRoute() {
|
|||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
) : ipv6mode === "static" ? (
|
||||
<StaticIpv6Card />
|
||||
) : (
|
||||
<Ipv6NetworkCard networkState={networkState || undefined} />
|
||||
)}
|
||||
</AutoHeight>
|
||||
</div>
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
{(formState.isDirty || formState.isSubmitting) && (
|
||||
<div className="animate-fadeInStill opacity-0 animation-duration-300">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
type="submit"
|
||||
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<div className="animate-fadeInStill opacity-0 animation-duration-300">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
type="submit"
|
||||
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
|
Loading…
Reference in New Issue