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 {
|
type IPv6StaticConfig struct {
|
||||||
Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"`
|
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"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,56 +33,54 @@ 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(
|
{networkState.ipv6_addresses.map(addr => (
|
||||||
addr => (
|
<div
|
||||||
<div
|
key={addr.address}
|
||||||
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"
|
||||||
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="grid grid-cols-2 gap-x-8 gap-y-4">
|
<div className="col-span-2 flex flex-col justify-between">
|
||||||
<div className="col-span-2 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">
|
Address
|
||||||
Address
|
</span>
|
||||||
</span>
|
<span className="text-sm font-medium">{addr.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>
|
||||||
|
|
||||||
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 validator from "validator";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { GridCard } from "@/components/Card";
|
import { GridCard } from "@/components/Card";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
|
@ -8,10 +8,15 @@ import { InputFieldWithLabel } from "@/components/InputField";
|
||||||
|
|
||||||
export default function StaticIpv4Card() {
|
export default function StaticIpv4Card() {
|
||||||
const formMethods = useFormContext();
|
const formMethods = useFormContext();
|
||||||
const { register, formState } = formMethods;
|
const { register, formState, watch } = 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">
|
||||||
|
@ -59,11 +64,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}
|
||||||
/>
|
/>
|
||||||
|
@ -92,6 +97,7 @@ 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>
|
||||||
|
|
|
@ -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[];
|
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;
|
||||||
|
@ -759,6 +766,7 @@ 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;
|
||||||
|
|
|
@ -21,6 +21,7 @@ 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";
|
||||||
|
|
||||||
|
@ -106,6 +107,13 @@ 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 };
|
||||||
|
@ -143,6 +151,12 @@ 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 => {
|
||||||
|
@ -160,7 +174,8 @@ export default function SettingsNetworkRoute() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isIPv4Mode = watch("ipv4_mode");
|
const ipv4mode = watch("ipv4_mode");
|
||||||
|
const ipv6mode = watch("ipv6_mode");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
|
@ -309,9 +324,9 @@ export default function SettingsNetworkRoute() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
) : isIPv4Mode === "static" ? (
|
) : ipv4mode === "static" ? (
|
||||||
<StaticIpv4Card />
|
<StaticIpv4Card />
|
||||||
) : isIPv4Mode === "dhcp" ? (
|
) : ipv4mode === "dhcp" ? (
|
||||||
<DhcpLeaseCard
|
<DhcpLeaseCard
|
||||||
networkState={networkState}
|
networkState={networkState}
|
||||||
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
||||||
|
@ -329,7 +344,10 @@ 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={[{ value: "slaac", label: "SLAAC" }]}
|
options={[
|
||||||
|
{ value: "slaac", label: "SLAAC" },
|
||||||
|
{ value: "static", label: "Static" },
|
||||||
|
]}
|
||||||
{...register("ipv6_mode")}
|
{...register("ipv6_mode")}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
@ -350,23 +368,27 @@ 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">
|
<>
|
||||||
<Button
|
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
size="SM"
|
<div className="animate-fadeInStill opacity-0 animation-duration-300">
|
||||||
theme="primary"
|
<Button
|
||||||
disabled={formState.isSubmitting}
|
size="SM"
|
||||||
loading={formState.isSubmitting}
|
theme="primary"
|
||||||
type="submit"
|
disabled={formState.isSubmitting}
|
||||||
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
loading={formState.isSubmitting}
|
||||||
/>
|
type="submit"
|
||||||
</div>
|
text={formState.isSubmitting ? "Saving..." : "Save Settings"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Reference in New Issue