mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-01-18 03:28:19 +01:00
feat(network): enhance network settings UI (#364)
* feat(network): enhance network settings UI with domain management and improved layout - Added custom domain input and selection options for DHCP and local domains. - Improved layout for displaying network settings, including DHCP lease information and IPv6 addresses. - Refactored state management for network settings and added handlers for hostname and domain changes. - Updated the display of network settings to enhance user experience and accessibility. * Re-add save button * fix: add ConfirmDialog for renewing DHCP lease and improve network settings layout - Integrated ConfirmDialog component to confirm DHCP lease renewal. - Enhanced the layout of network settings, including better organization of IPv4 and IPv6 information. - Updated state management for displaying network settings and lease information. - Improved user experience with clearer descriptions and structured UI elements. * Fix lint errors * fix: useRef TS2554 --------- Co-authored-by: Siyuan Miao <i@xswan.net>
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
import { useEffect, Fragment, useMemo, useState, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LuPenLine, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown, LuTrash2, LuCommand } from "react-icons/lu";
|
||||
import {
|
||||
LuPenLine,
|
||||
LuCopy,
|
||||
LuMoveRight,
|
||||
LuCornerDownRight,
|
||||
LuArrowUp,
|
||||
LuArrowDown,
|
||||
LuTrash2,
|
||||
LuCommand,
|
||||
} from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
@@ -26,10 +35,10 @@ export default function SettingsMacrosRoute() {
|
||||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
|
||||
|
||||
const isMaxMacrosReached = useMemo(() =>
|
||||
macros.length >= MAX_TOTAL_MACROS,
|
||||
[macros.length]
|
||||
|
||||
const isMaxMacrosReached = useMemo(
|
||||
() => macros.length >= MAX_TOTAL_MACROS,
|
||||
[macros.length],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -38,75 +47,83 @@ export default function SettingsMacrosRoute() {
|
||||
}
|
||||
}, [initialized, loadMacros]);
|
||||
|
||||
const handleDuplicateMacro = useCallback(async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaxMacrosReached) {
|
||||
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionLoadingId(macro.id);
|
||||
|
||||
const newMacroCopy: KeySequence = {
|
||||
...JSON.parse(JSON.stringify(macro)),
|
||||
id: generateMacroId(),
|
||||
name: `${macro.name} ${COPY_SUFFIX}`,
|
||||
sortOrder: macros.length + 1,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveMacros(normalizeSortOrders([...macros, newMacroCopy]));
|
||||
notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to duplicate macro: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to duplicate macro");
|
||||
const handleDuplicateMacro = useCallback(
|
||||
async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
}, [isMaxMacrosReached, macros, saveMacros, setActionLoadingId]);
|
||||
|
||||
const handleMoveMacro = useCallback(async (index: number, direction: 'up' | 'down', macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= macros.length) return;
|
||||
|
||||
setActionLoadingId(macroId);
|
||||
|
||||
try {
|
||||
const newMacros = [...macros];
|
||||
[newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]];
|
||||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
if (isMaxMacrosReached) {
|
||||
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
}, [macros, saveMacros, setActionLoadingId]);
|
||||
|
||||
setActionLoadingId(macro.id);
|
||||
|
||||
const newMacroCopy: KeySequence = {
|
||||
...JSON.parse(JSON.stringify(macro)),
|
||||
id: generateMacroId(),
|
||||
name: `${macro.name} ${COPY_SUFFIX}`,
|
||||
sortOrder: macros.length + 1,
|
||||
};
|
||||
|
||||
try {
|
||||
await saveMacros(normalizeSortOrders([...macros, newMacroCopy]));
|
||||
notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to duplicate macro: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to duplicate macro");
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
},
|
||||
[isMaxMacrosReached, macros, saveMacros, setActionLoadingId],
|
||||
);
|
||||
|
||||
const handleMoveMacro = useCallback(
|
||||
async (index: number, direction: "up" | "down", macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= macros.length) return;
|
||||
|
||||
setActionLoadingId(macroId);
|
||||
|
||||
try {
|
||||
const newMacros = [...macros];
|
||||
[newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]];
|
||||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
},
|
||||
[macros, saveMacros, setActionLoadingId],
|
||||
);
|
||||
|
||||
const handleDeleteMacro = useCallback(async () => {
|
||||
if (!macroToDelete?.id) return;
|
||||
|
||||
setActionLoadingId(macroToDelete.id);
|
||||
try {
|
||||
const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macroToDelete.id));
|
||||
const updatedMacros = normalizeSortOrders(
|
||||
macros.filter(m => m.id !== macroToDelete.id),
|
||||
);
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success(`Macro "${macroToDelete.name}" deleted successfully`);
|
||||
setShowDeleteConfirm(false);
|
||||
@@ -122,135 +139,168 @@ export default function SettingsMacrosRoute() {
|
||||
}
|
||||
}, [macroToDelete, macros, saveMacros]);
|
||||
|
||||
const MacroList = useMemo(() => (
|
||||
<div className="space-y-2">
|
||||
{macros.map((macro, index) => (
|
||||
<Card key={macro.id} className="p-2 bg-white dark:bg-slate-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'up', macro.id)}
|
||||
disabled={index === 0 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowUp}
|
||||
aria-label={`Move ${macro.name} up`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'down', macro.id)}
|
||||
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowDown}
|
||||
aria-label={`Move ${macro.name} down`}
|
||||
/>
|
||||
</div>
|
||||
const MacroList = useMemo(
|
||||
() => (
|
||||
<div className="space-y-2">
|
||||
{macros.map((macro, index) => (
|
||||
<Card key={macro.id} className="bg-white p-2 dark:bg-slate-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, "up", macro.id)}
|
||||
disabled={index === 0 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowUp}
|
||||
aria-label={`Move ${macro.name} up`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, "down", macro.id)}
|
||||
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowDown}
|
||||
aria-label={`Move ${macro.name} down`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center ml-2">
|
||||
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
|
||||
{macro.name}
|
||||
</h3>
|
||||
<p className="mt-1 ml-4 text-xs text-slate-500 dark:text-slate-400 overflow-hidden">
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
{macro.steps.map((step, stepIndex) => {
|
||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||
<div className="ml-2 flex min-w-0 flex-1 flex-col justify-center">
|
||||
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
|
||||
{macro.name}
|
||||
</h3>
|
||||
<p className="ml-4 mt-1 overflow-hidden text-xs text-slate-500 dark:text-slate-400">
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
{macro.steps.map((step, stepIndex) => {
|
||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||
|
||||
return (
|
||||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 text-slate-400 dark:text-slate-500 h-3 w-3 flex-shrink-0" />
|
||||
<span className="bg-slate-50 dark:bg-slate-800 px-2 py-0.5 rounded-md border border-slate-200/50 dark:border-slate-700/50">
|
||||
{(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => (
|
||||
<Fragment key={`mod-${idx}`}>
|
||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||
{modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
return (
|
||||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 h-3 w-3 flex-shrink-0 text-slate-400 dark:text-slate-500" />
|
||||
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
|
||||
{(Array.isArray(step.modifiers) &&
|
||||
step.modifiers.length > 0) ||
|
||||
(Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) &&
|
||||
step.modifiers.map((modifier, idx) => (
|
||||
<Fragment key={`mod-${idx}`}>
|
||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||
{modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{Array.isArray(step.modifiers) &&
|
||||
step.modifiers.length > 0 &&
|
||||
Array.isArray(step.keys) &&
|
||||
step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
|
||||
{Array.isArray(step.keys) && step.keys.map((key, idx) => (
|
||||
<Fragment key={`key-${idx}`}>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||
{keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">Delay only</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">({step.delay}ms)</span>
|
||||
)}
|
||||
{Array.isArray(step.keys) &&
|
||||
step.keys.map((key, idx) => (
|
||||
<Fragment key={`key-${idx}`}>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||
{keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600">
|
||||
{" "}
|
||||
+{" "}
|
||||
</span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">
|
||||
Delay only
|
||||
</span>
|
||||
)}
|
||||
{step.delay !== DEFAULT_DELAY && (
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">
|
||||
({step.delay}ms)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
<Button
|
||||
size="XS"
|
||||
className="text-red-500 dark:text-red-400"
|
||||
theme="light"
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={() => {
|
||||
setMacroToDelete(macro);
|
||||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Delete macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuCopy}
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
text="Edit"
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Edit macro ${macro.name}`}
|
||||
/>
|
||||
<div className="ml-4 flex items-center gap-1">
|
||||
<Button
|
||||
size="XS"
|
||||
className="text-red-500 dark:text-red-400"
|
||||
theme="light"
|
||||
LeadingIcon={LuTrash2}
|
||||
onClick={() => {
|
||||
setMacroToDelete(macro);
|
||||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Delete macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuCopy}
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
text="Edit"
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Edit macro ${macro.name}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<ConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onClose={() => {
|
||||
setShowDeleteConfirm(false);
|
||||
setMacroToDelete(null);
|
||||
}}
|
||||
title="Delete Macro"
|
||||
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
|
||||
variant="danger"
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
|
||||
onConfirm={handleDeleteMacro}
|
||||
isConfirming={actionLoadingId === macroToDelete?.id}
|
||||
/>
|
||||
</div>
|
||||
), [macros, actionLoadingId, showDeleteConfirm, macroToDelete, handleDeleteMacro]);
|
||||
<ConfirmDialog
|
||||
open={showDeleteConfirm}
|
||||
onClose={() => {
|
||||
setShowDeleteConfirm(false);
|
||||
setMacroToDelete(null);
|
||||
}}
|
||||
title="Delete Macro"
|
||||
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
|
||||
variant="danger"
|
||||
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
|
||||
onConfirm={handleDeleteMacro}
|
||||
isConfirming={actionLoadingId === macroToDelete?.id}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
[
|
||||
macros,
|
||||
showDeleteConfirm,
|
||||
macroToDelete?.name,
|
||||
macroToDelete?.id,
|
||||
actionLoadingId,
|
||||
handleDeleteMacro,
|
||||
handleMoveMacro,
|
||||
handleDuplicateMacro,
|
||||
navigate,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -259,7 +309,7 @@ export default function SettingsMacrosRoute() {
|
||||
title="Keyboard Macros"
|
||||
description={`Combine keystrokes into a single action for faster workflows.`}
|
||||
/>
|
||||
{ macros.length > 0 && (
|
||||
{macros.length > 0 && (
|
||||
<div className="flex items-center pl-2">
|
||||
<Button
|
||||
size="SM"
|
||||
@@ -299,7 +349,9 @@ export default function SettingsMacrosRoute() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : MacroList}
|
||||
) : (
|
||||
MacroList
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
|
||||
import { IPv4Mode, IPv6Mode, LLDPMode, mDNSMode, NetworkSettings, NetworkState, TimeSyncMode, useNetworkStateStore } from "@/hooks/stores";
|
||||
import {
|
||||
IPv4Mode,
|
||||
IPv6Mode,
|
||||
LLDPMode,
|
||||
mDNSMode,
|
||||
NetworkSettings,
|
||||
NetworkState,
|
||||
TimeSyncMode,
|
||||
useNetworkStateStore,
|
||||
} from "@/hooks/stores";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import InputField from "@components/InputField";
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
import Fieldset from "../components/Fieldset";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -25,13 +37,9 @@ const defaultNetworkSettings: NetworkSettings = {
|
||||
lldp_tx_tlvs: [],
|
||||
mdns_mode: "unknown",
|
||||
time_sync_mode: "unknown",
|
||||
}
|
||||
};
|
||||
|
||||
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||
if (lifetime == "") {
|
||||
return <strong>N/A</strong>;
|
||||
}
|
||||
|
||||
const [remaining, setRemaining] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,46 +51,87 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||
return () => clearInterval(interval);
|
||||
}, [lifetime]);
|
||||
|
||||
return <>
|
||||
<strong>{dayjs(lifetime).format()}</strong>
|
||||
{remaining && <>
|
||||
{" "}<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
({remaining})
|
||||
</span>
|
||||
</>}
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
<span>{dayjs(lifetime).format("YYYY-MM-DD HH:mm")}</span>
|
||||
{remaining && (
|
||||
<>
|
||||
{" "}
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
({remaining})
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsNetworkRoute() {
|
||||
const [send] = useJsonRpc();
|
||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [state, state.setNetworkState]);
|
||||
const [networkState, setNetworkState] = useNetworkStateStore(state => [
|
||||
state,
|
||||
state.setNetworkState,
|
||||
]);
|
||||
|
||||
const [networkSettings, setNetworkSettings] =
|
||||
useState<NetworkSettings>(defaultNetworkSettings);
|
||||
|
||||
// We use this to determine whether the settings have changed
|
||||
const firstNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
|
||||
|
||||
const [networkSettings, setNetworkSettings] = useState<NetworkSettings>(defaultNetworkSettings);
|
||||
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
|
||||
|
||||
const [customDomain, setCustomDomain] = useState<string>("");
|
||||
const [selectedDomainOption, setSelectedDomainOption] = useState<string>("dhcp");
|
||||
|
||||
useEffect(() => {
|
||||
if (networkSettings.domain && networkSettingsLoaded) {
|
||||
// Check if the domain is one of the predefined options
|
||||
const predefinedOptions = ["dhcp", "local"];
|
||||
if (predefinedOptions.includes(networkSettings.domain)) {
|
||||
setSelectedDomainOption(networkSettings.domain);
|
||||
} else {
|
||||
setSelectedDomainOption("custom");
|
||||
setCustomDomain(networkSettings.domain);
|
||||
}
|
||||
}
|
||||
}, [networkSettings.domain, networkSettingsLoaded]);
|
||||
|
||||
const getNetworkSettings = useCallback(() => {
|
||||
setNetworkSettingsLoaded(false);
|
||||
send("getNetworkSettings", {}, resp => {
|
||||
if ("error" in resp) return;
|
||||
console.log(resp.result);
|
||||
setNetworkSettings(resp.result as NetworkSettings);
|
||||
|
||||
if (!firstNetworkSettings.current) {
|
||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
||||
}
|
||||
setNetworkSettingsLoaded(true);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const setNetworkSettingsRemote = useCallback((settings: NetworkSettings) => {
|
||||
setNetworkSettingsLoaded(false);
|
||||
send("setNetworkSettings", { settings }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error("Failed to save network settings: " + (resp.error.data ? resp.error.data : resp.error.message));
|
||||
const setNetworkSettingsRemote = useCallback(
|
||||
(settings: NetworkSettings) => {
|
||||
setNetworkSettingsLoaded(false);
|
||||
send("setNetworkSettings", { settings }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
"Failed to save network settings: " +
|
||||
(resp.error.data ? resp.error.data : resp.error.message),
|
||||
);
|
||||
setNetworkSettingsLoaded(true);
|
||||
return;
|
||||
}
|
||||
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
||||
setNetworkSettings(resp.result as NetworkSettings);
|
||||
setNetworkSettingsLoaded(true);
|
||||
return;
|
||||
}
|
||||
setNetworkSettings(resp.result as NetworkSettings);
|
||||
setNetworkSettingsLoaded(true);
|
||||
notifications.success("Network settings saved");
|
||||
});
|
||||
}, [send]);
|
||||
notifications.success("Network settings saved");
|
||||
});
|
||||
},
|
||||
[send],
|
||||
);
|
||||
|
||||
const getNetworkState = useCallback(() => {
|
||||
send("getNetworkState", {}, resp => {
|
||||
@@ -90,7 +139,7 @@ export default function SettingsNetworkRoute() {
|
||||
console.log(resp.result);
|
||||
setNetworkState(resp.result as NetworkState);
|
||||
});
|
||||
}, [send]);
|
||||
}, [send, setNetworkState]);
|
||||
|
||||
const handleRenewLease = useCallback(() => {
|
||||
send("renewDHCPLease", {}, resp => {
|
||||
@@ -131,278 +180,520 @@ export default function SettingsNetworkRoute() {
|
||||
setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode });
|
||||
};
|
||||
|
||||
const filterUnknown = useCallback((options: { value: string; label: string; }[]) => {
|
||||
if (!networkSettingsLoaded) return options;
|
||||
return options.filter(option => option.value !== "unknown");
|
||||
}, [networkSettingsLoaded]);
|
||||
const handleHostnameChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, hostname: value });
|
||||
};
|
||||
|
||||
const handleDomainChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, domain: value });
|
||||
};
|
||||
|
||||
const handleDomainOptionChange = (value: string) => {
|
||||
setSelectedDomainOption(value);
|
||||
if (value !== "custom") {
|
||||
handleDomainChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomDomainChange = (value: string) => {
|
||||
setCustomDomain(value);
|
||||
handleDomainChange(value);
|
||||
};
|
||||
|
||||
const filterUnknown = useCallback(
|
||||
(options: { value: string; label: string }[]) => {
|
||||
if (!networkSettingsLoaded) return options;
|
||||
return options.filter(option => option.value !== "unknown");
|
||||
},
|
||||
[networkSettingsLoaded],
|
||||
);
|
||||
|
||||
const [showRenewLeaseConfirm, setShowRenewLeaseConfirm] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Network"
|
||||
description="Configure your network settings"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="MAC Address"
|
||||
description={<></>}
|
||||
>
|
||||
<span className="select-auto font-mono text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.mac_address}
|
||||
</span>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Hostname"
|
||||
description={
|
||||
<>
|
||||
Hostname for the device
|
||||
<br />
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
Leave blank for default
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputField
|
||||
type="text"
|
||||
placeholder="jetkvm"
|
||||
value={networkSettings.hostname}
|
||||
error={""}
|
||||
onChange={e => {
|
||||
setNetworkSettings({ ...networkSettings, hostname: e.target.value });
|
||||
}}
|
||||
disabled={!networkSettingsLoaded}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Domain"
|
||||
description={
|
||||
<>
|
||||
Domain for the device
|
||||
<br />
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
Leave blank to use DHCP provided domain, if there is no domain, use <span className="font-mono">local</span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputField
|
||||
type="text"
|
||||
placeholder="local"
|
||||
value={networkSettings.domain}
|
||||
error={""}
|
||||
onChange={e => {
|
||||
setNetworkSettings({ ...networkSettings, domain: e.target.value });
|
||||
}}
|
||||
disabled={!networkSettingsLoaded}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="IPv4 Mode"
|
||||
description="Configure the IPv4 mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv4_mode}
|
||||
onChange={e => handleIpv4ModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "dhcp", label: "DHCP" },
|
||||
// { value: "static", label: "Static" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.dhcp_lease && (
|
||||
<GridCard>
|
||||
<div className="flex items-start gap-x-4 p-4">
|
||||
<div className="space-y-3 w-full">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
Current DHCP Lease
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-none space-y-1 text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.dhcp_lease?.ip && <li>IP: <strong>{networkState?.dhcp_lease?.ip}</strong></li>}
|
||||
{networkState?.dhcp_lease?.netmask && <li>Subnet: <strong>{networkState?.dhcp_lease?.netmask}</strong></li>}
|
||||
{networkState?.dhcp_lease?.broadcast && <li>Broadcast: <strong>{networkState?.dhcp_lease?.broadcast}</strong></li>}
|
||||
{networkState?.dhcp_lease?.ttl && <li>TTL: <strong>{networkState?.dhcp_lease?.ttl}</strong></li>}
|
||||
{networkState?.dhcp_lease?.mtu && <li>MTU: <strong>{networkState?.dhcp_lease?.mtu}</strong></li>}
|
||||
{networkState?.dhcp_lease?.hostname && <li>Hostname: <strong>{networkState?.dhcp_lease?.hostname}</strong></li>}
|
||||
{networkState?.dhcp_lease?.domain && <li>Domain: <strong>{networkState?.dhcp_lease?.domain}</strong></li>}
|
||||
{networkState?.dhcp_lease?.routers && <li>Gateway: <strong>{networkState?.dhcp_lease?.routers.join(", ")}</strong></li>}
|
||||
{networkState?.dhcp_lease?.dns && <li>DNS: <strong>{networkState?.dhcp_lease?.dns.join(", ")}</strong></li>}
|
||||
{networkState?.dhcp_lease?.ntp_servers && <li>NTP Servers: <strong>{networkState?.dhcp_lease?.ntp_servers.join(", ")}</strong></li>}
|
||||
{networkState?.dhcp_lease?.server_id && <li>Server ID: <strong>{networkState?.dhcp_lease?.server_id}</strong></li>}
|
||||
{networkState?.dhcp_lease?.bootp_next_server && <li>BootP Next Server: <strong>{networkState?.dhcp_lease?.bootp_next_server}</strong></li>}
|
||||
{networkState?.dhcp_lease?.bootp_server_name && <li>BootP Server Name: <strong>{networkState?.dhcp_lease?.bootp_server_name}</strong></li>}
|
||||
{networkState?.dhcp_lease?.bootp_file && <li>Boot File: <strong>{networkState?.dhcp_lease?.bootp_file}</strong></li>}
|
||||
{networkState?.dhcp_lease?.lease_expiry && <li>
|
||||
Lease Expiry: <LifeTimeLabel lifetime={`${networkState?.dhcp_lease?.lease_expiry}`} />
|
||||
</li>}
|
||||
{/* {JSON.stringify(networkState?.dhcp_lease)} */}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="block w-full dark:border-slate-600" />
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Renew lease"
|
||||
onClick={() => {
|
||||
handleRenewLease();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<Fieldset disabled={!networkSettingsLoaded} className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Network"
|
||||
description="Configure your network settings"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="MAC Address"
|
||||
description="Hardware identifier for the network interface"
|
||||
>
|
||||
<InputField
|
||||
type="text"
|
||||
size="SM"
|
||||
value={networkState?.mac_address}
|
||||
error={""}
|
||||
disabled={true}
|
||||
readOnly={true}
|
||||
className="dark:!text-opacity-60"
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Hostname"
|
||||
description="Device identifier on the network. Blank for system default"
|
||||
>
|
||||
<div className="relative">
|
||||
<div>
|
||||
<InputField
|
||||
size="SM"
|
||||
type="text"
|
||||
placeholder="jetkvm"
|
||||
defaultValue={networkSettings.hostname}
|
||||
onChange={e => {
|
||||
handleHostnameChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="IPv6 Mode"
|
||||
description="Configure the IPv6 mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Domain"
|
||||
description="Network domain suffix for the device"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={selectedDomainOption}
|
||||
onChange={e => handleDomainOptionChange(e.target.value)}
|
||||
options={[
|
||||
{ value: "dhcp", label: "DHCP provided" },
|
||||
{ value: "local", label: ".local" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
{selectedDomainOption === "custom" && (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
<InputField
|
||||
size="SM"
|
||||
type="text"
|
||||
placeholder="home"
|
||||
value={customDomain}
|
||||
onChange={e => setCustomDomain(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Save Domain"
|
||||
onClick={() => handleCustomDomainChange(customDomain)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="mDNS"
|
||||
description="Control mDNS (multicast DNS) operational mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.mdns_mode}
|
||||
onChange={e => handleMdnsModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "auto", label: "Auto" },
|
||||
{ value: "ipv4_only", label: "IPv4 only" },
|
||||
{ value: "ipv6_only", label: "IPv6 only" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Time synchronization"
|
||||
description="Configure time synchronization settings"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.time_sync_mode}
|
||||
onChange={e => {
|
||||
handleTimeSyncModeChange(e.target.value);
|
||||
}}
|
||||
options={filterUnknown([
|
||||
{ value: "unknown", label: "..." },
|
||||
// { value: "auto", label: "Auto" },
|
||||
{ value: "ntp_only", label: "NTP only" },
|
||||
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
||||
{ value: "http_only", label: "HTTP only" },
|
||||
// { value: "custom", label: "Custom" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="SM"
|
||||
value={networkSettings.ipv6_mode}
|
||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
// { value: "disabled", label: "Disabled" },
|
||||
{ value: "slaac", label: "SLAAC" },
|
||||
// { value: "dhcpv6", label: "DHCPv6" },
|
||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
||||
// { value: "static", label: "Static" },
|
||||
// { value: "link_local", label: "Link-local only" },
|
||||
])}
|
||||
theme="primary"
|
||||
disabled={firstNetworkSettings.current === networkSettings}
|
||||
text="Save Settings"
|
||||
onClick={() => setNetworkSettingsRemote(networkSettings)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.ipv6_addresses && (
|
||||
<GridCard>
|
||||
<div className="flex items-start gap-x-4 p-4">
|
||||
<div className="space-y-3 w-full">
|
||||
<div className="space-y-2">
|
||||
</div>
|
||||
|
||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="IPv4 Mode" description="Configure the IPv4 mode">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv4_mode}
|
||||
onChange={e => handleIpv4ModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
{ value: "dhcp", label: "DHCP" },
|
||||
// { value: "static", label: "Static" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.dhcp_lease && (
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
DHCP Lease
|
||||
</h3>
|
||||
|
||||
<div className="flex gap-x-6 gap-y-2">
|
||||
<div className="flex-1 space-y-2">
|
||||
{networkState?.dhcp_lease?.ip && (
|
||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
IP Address
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ip}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.netmask && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Subnet Mask
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.netmask}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.dns && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
DNS Servers
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.dns.map(dns => (
|
||||
<div key={dns}>{dns}</div>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.broadcast && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Broadcast
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.broadcast}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.domain && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Domain
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.domain}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.ntp_servers &&
|
||||
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
|
||||
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
||||
NTP Servers
|
||||
</div>
|
||||
<div className="shrink text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
||||
<div key={server}>{server}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.hostname && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Hostname
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.hostname}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-2">
|
||||
{networkState?.dhcp_lease?.routers &&
|
||||
networkState?.dhcp_lease?.routers.length > 0 && (
|
||||
<div className="flex justify-between pt-2">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Gateway
|
||||
</span>
|
||||
<span className="text-right text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.routers.map(router => (
|
||||
<div key={router}>{router}</div>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.server_id && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
DHCP Server
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.server_id}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.lease_expiry && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Lease Expires
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
<LifeTimeLabel
|
||||
lifetime={`${networkState?.dhcp_lease?.lease_expiry}`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.mtu && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
MTU
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.mtu}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.ttl && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
TTL
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.ttl}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.bootp_next_server && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Boot Next Server
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_next_server}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.bootp_server_name && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Boot Server Name
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_server_name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{networkState?.dhcp_lease?.bootp_file && (
|
||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Boot File
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.dhcp_lease?.bootp_file}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
className="text-red-500"
|
||||
text="Renew DHCP Lease"
|
||||
LeadingIcon={ArrowPathIcon}
|
||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.ipv6_mode}
|
||||
onChange={e => handleIpv6ModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
// { value: "disabled", label: "Disabled" },
|
||||
{ value: "slaac", label: "SLAAC" },
|
||||
// { value: "dhcpv6", label: "DHCPv6" },
|
||||
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
||||
// { value: "static", label: "Static" },
|
||||
// { value: "link_local", label: "Link-local only" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{networkState?.ipv6_addresses && (
|
||||
<GridCard>
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Information
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Link-local
|
||||
</h4>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.ipv6_link_local}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-900 dark:text-white">
|
||||
IPv6 Addresses
|
||||
</h4>
|
||||
<ul className="list-none space-y-1 text-xs text-slate-700 dark:text-slate-300">
|
||||
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.map(addr => (
|
||||
<li key={addr.address}>
|
||||
{addr.address}
|
||||
{addr.valid_lifetime && <>
|
||||
<br />
|
||||
- valid_lft: {" "}
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||
</span>
|
||||
</>}
|
||||
{addr.preferred_lifetime && <>
|
||||
<br />
|
||||
- pref_lft: {" "}
|
||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||
</span>
|
||||
</>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||
{networkState?.dhcp_lease?.ip && (
|
||||
<div className="flex flex-col justify-between">
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Link-local
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{networkState?.ipv6_link_local}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
{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-slate-100/40 p-4 pl-4 dark:border-blue-500 dark:bg-slate-900"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4 hidden">
|
||||
<SettingsItem
|
||||
title="LLDP"
|
||||
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.lldp_mode}
|
||||
onChange={e => handleLldpModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "basic", label: "Basic" },
|
||||
{ value: "all", label: "All" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="mDNS"
|
||||
description="Control mDNS (multicast DNS) operational mode"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.mdns_mode}
|
||||
onChange={e => handleMdnsModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "auto", label: "Auto" },
|
||||
{ value: "ipv4_only", label: "IPv4 only" },
|
||||
{ value: "ipv6_only", label: "IPv6 only" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Time synchronization"
|
||||
description="Configure time synchronization settings"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.time_sync_mode}
|
||||
onChange={e => handleTimeSyncModeChange(e.target.value)}
|
||||
disabled={!networkSettingsLoaded}
|
||||
options={filterUnknown([
|
||||
{ value: "unknown", label: "..." },
|
||||
// { value: "auto", label: "Auto" },
|
||||
{ value: "ntp_only", label: "NTP only" },
|
||||
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
||||
{ value: "http_only", label: "HTTP only" },
|
||||
// { value: "custom", label: "Custom" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="flex items-end gap-x-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setNetworkSettingsRemote(networkSettings);
|
||||
}}
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Save Settings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden space-y-4">
|
||||
<SettingsItem
|
||||
title="LLDP"
|
||||
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={networkSettings.lldp_mode}
|
||||
onChange={e => handleLldpModeChange(e.target.value)}
|
||||
options={filterUnknown([
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "basic", label: "Basic" },
|
||||
{ value: "all", label: "All" },
|
||||
])}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</Fieldset>
|
||||
<ConfirmDialog
|
||||
open={showRenewLeaseConfirm}
|
||||
onClose={() => setShowRenewLeaseConfirm(false)}
|
||||
title="Renew DHCP Lease"
|
||||
description="This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process."
|
||||
variant="danger"
|
||||
confirmText="Renew Lease"
|
||||
onConfirm={() => {
|
||||
handleRenewLease();
|
||||
setShowRenewLeaseConfirm(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user