mirror of https://github.com/jetkvm/kvm.git
feat: add copy to clipboard functionality for MAC address in network settings
This commit is contained in:
parent
76d256b69a
commit
5e06625966
|
|
@ -28,6 +28,7 @@
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router": "^7.9.3",
|
"react-router": "^7.9.3",
|
||||||
|
|
@ -5856,8 +5857,6 @@
|
||||||
"react": "^19.1.1"
|
"react": "^19.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.62.0",
|
"version": "7.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||||
|
|
@ -5874,7 +5873,6 @@
|
||||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
>>>>>>> Stashed changes
|
|
||||||
"node_modules/react-hot-toast": {
|
"node_modules/react-hot-toast": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
export function useCopyToClipboard(resetInterval = 2000) {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
|
const copy = useCallback(async (text: string) => {
|
||||||
|
if (!text) return false;
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
success = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Clipboard API failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for insecure contexts
|
||||||
|
if (!success) {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.opacity = "0";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
success = document.execCommand("copy");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fallback copy failed:", err);
|
||||||
|
success = false;
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCopied(success);
|
||||||
|
if (success && resetInterval > 0) {
|
||||||
|
setTimeout(() => setIsCopied(false), resetInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}, [resetInterval]);
|
||||||
|
|
||||||
|
return { copy, isCopied };
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { FieldValues, FormProvider, useForm } from "react-hook-form";
|
import { FieldValues, FormProvider, useForm } from "react-hook-form";
|
||||||
import { LuEthernetPort } from "react-icons/lu";
|
import { LuCopy, LuEthernetPort } from "react-icons/lu";
|
||||||
import validator from "validator";
|
import validator from "validator";
|
||||||
|
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
|
|
@ -24,6 +24,7 @@ import StaticIpv4Card from "../components/StaticIpv4Card";
|
||||||
import StaticIpv6Card from "../components/StaticIpv6Card";
|
import StaticIpv6Card from "../components/StaticIpv6Card";
|
||||||
import { useJsonRpc } from "../hooks/useJsonRpc";
|
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||||
import { SettingsItem } from "../components/SettingsItem";
|
import { SettingsItem } from "../components/SettingsItem";
|
||||||
|
import { useCopyToClipboard } from "../components/useCopyToClipBoard";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
|
@ -225,6 +226,8 @@ export default function SettingsNetworkRoute() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { copy } = useCopyToClipboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
|
|
@ -248,19 +251,26 @@ export default function SettingsNetworkRoute() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<div className="flex items-center justify-between">
|
||||||
title="MAC Address"
|
<SettingsItem
|
||||||
description="Hardware identifier for the network interface"
|
title="MAC Address"
|
||||||
>
|
description="Hardware identifier for the network interface"
|
||||||
<InputField
|
|
||||||
type="text"
|
|
||||||
size="SM"
|
|
||||||
value={networkState?.mac_address}
|
|
||||||
error={""}
|
|
||||||
readOnly={true}
|
|
||||||
className="dark:!text-opacity-60"
|
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
<div className="flex items-center">
|
||||||
|
<GridCard cardClassName="rounded-r-none">
|
||||||
|
<div className=" h-[34px] flex items-center text-xs select-all text-black font-mono dark:text-white px-3 ">
|
||||||
|
{networkState?.mac_address} {" "}
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
<Button className="rounded-l-none border-l-blue-900 dark:border-l-blue-600" size="SM" type="button" theme="primary" LeadingIcon={LuCopy} onClick={async () => {
|
||||||
|
if (await copy(networkState?.mac_address || "")) {
|
||||||
|
notifications.success("MAC address copied to clipboard");
|
||||||
|
} else {
|
||||||
|
notifications.error("Failed to copy MAC address");
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<SettingsItem title="Hostname" description="Set the device hostname">
|
<SettingsItem title="Hostname" description="Set the device hostname">
|
||||||
<InputField
|
<InputField
|
||||||
size="SM"
|
size="SM"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue