mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-25 00:46:21 +00:00
update virtual-machines.tsx
This commit is contained in:
@@ -948,23 +948,31 @@ export function Security() {
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
// Preferred path (HTTPS / localhost). On plain HTTP the Promise rejects,
|
||||
// so we catch and fall through to the textarea fallback.
|
||||
try {
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = text
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.left = "-9999px"
|
||||
textarea.style.top = "-9999px"
|
||||
textarea.style.opacity = "0"
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
document.execCommand("copy")
|
||||
document.body.removeChild(textarea)
|
||||
return true
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
// fall through to execCommand fallback
|
||||
}
|
||||
|
||||
try {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = text
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.left = "-9999px"
|
||||
textarea.style.top = "-9999px"
|
||||
textarea.style.opacity = "0"
|
||||
textarea.readOnly = true
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
const ok = document.execCommand("copy")
|
||||
document.body.removeChild(textarea)
|
||||
return ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -293,10 +293,44 @@ export function Settings() {
|
||||
}
|
||||
}
|
||||
|
||||
const copySourceCode = () => {
|
||||
navigator.clipboard.writeText(codeModal.source)
|
||||
setCodeCopied(true)
|
||||
setTimeout(() => setCodeCopied(false), 2000)
|
||||
const copySourceCode = async () => {
|
||||
const text = codeModal.source
|
||||
let ok = false
|
||||
|
||||
// Preferred path (HTTPS / localhost). On plain HTTP the Promise rejects,
|
||||
// so we catch and fall through to the textarea fallback.
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ok = true
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
try {
|
||||
const ta = document.createElement("textarea")
|
||||
ta.value = text
|
||||
ta.style.position = "fixed"
|
||||
ta.style.left = "-9999px"
|
||||
ta.style.top = "-9999px"
|
||||
ta.style.opacity = "0"
|
||||
ta.readOnly = true
|
||||
document.body.appendChild(ta)
|
||||
ta.focus()
|
||||
ta.select()
|
||||
ok = document.execCommand("copy")
|
||||
document.body.removeChild(ta)
|
||||
} catch {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
setCodeCopied(true)
|
||||
setTimeout(() => setCodeCopied(false), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const changeNetworkUnit = (unit: string) => {
|
||||
|
||||
@@ -90,33 +90,49 @@ export function TwoFactorSetup({ open, onClose, onSuccess }: TwoFactorSetupProps
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string, type: "secret" | "codes") => {
|
||||
let ok = false
|
||||
|
||||
// Preferred path (HTTPS / localhost). On plain HTTP the Promise rejects,
|
||||
// so we catch and fall through to the textarea fallback.
|
||||
try {
|
||||
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
// Fallback for non-secure contexts (HTTP)
|
||||
ok = true
|
||||
}
|
||||
} catch {
|
||||
// fall through to execCommand fallback
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
try {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = text
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.left = "-9999px"
|
||||
textarea.style.top = "-9999px"
|
||||
textarea.style.opacity = "0"
|
||||
textarea.readOnly = true
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
document.execCommand("copy")
|
||||
ok = document.execCommand("copy")
|
||||
document.body.removeChild(textarea)
|
||||
} catch {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "secret") {
|
||||
setCopiedSecret(true)
|
||||
setTimeout(() => setCopiedSecret(false), 2000)
|
||||
} else {
|
||||
setCopiedCodes(true)
|
||||
setTimeout(() => setCopiedCodes(false), 2000)
|
||||
}
|
||||
} catch {
|
||||
if (!ok) {
|
||||
console.error("Failed to copy to clipboard")
|
||||
return
|
||||
}
|
||||
|
||||
if (type === "secret") {
|
||||
setCopiedSecret(true)
|
||||
setTimeout(() => setCopiedSecret(false), 2000)
|
||||
} else {
|
||||
setCopiedCodes(true)
|
||||
setTimeout(() => setCopiedCodes(false), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -295,10 +295,10 @@ export function VirtualMachines() {
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useSWR<VMData[]>("/api/vms", fetcher, {
|
||||
refreshInterval: 5000,
|
||||
refreshInterval: 2500,
|
||||
revalidateOnFocus: true,
|
||||
revalidateOnReconnect: true,
|
||||
dedupingInterval: 2000,
|
||||
dedupingInterval: 1000,
|
||||
errorRetryCount: 2,
|
||||
})
|
||||
|
||||
@@ -423,36 +423,16 @@ export function VirtualMachines() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Keep the open modal's VM in sync with the 5s poll of /api/vms so CPU/RAM/I-O
|
||||
// values don't stay frozen at click-time while the user has the modal open.
|
||||
// Keep the open modal's VM in sync with the /api/vms poll so CPU/RAM/I-O values
|
||||
// don't stay frozen at click-time. Single data source (/cluster/resources) shared
|
||||
// with the list — no source mismatch, no flicker.
|
||||
useEffect(() => {
|
||||
if (!selectedVM || !vmData) return
|
||||
const updated = vmData.find((v) => v.vmid === selectedVM.vmid)
|
||||
if (!updated) return
|
||||
// Avoid unnecessary setState when no field changed (reference-equal shortcut first).
|
||||
if (updated === selectedVM) return
|
||||
if (!updated || updated === selectedVM) return
|
||||
setSelectedVM(updated)
|
||||
}, [vmData])
|
||||
|
||||
// Faster per-VM live status poll that only runs while the modal is open.
|
||||
// SWR disables polling when the key is null, so this is truly scoped to the modal.
|
||||
const { data: liveVMStatus } = useSWR<VMData>(
|
||||
selectedVM ? `/api/vms/${selectedVM.vmid}/status` : null,
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 2500,
|
||||
revalidateOnFocus: true,
|
||||
revalidateOnReconnect: true,
|
||||
dedupingInterval: 1000,
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!liveVMStatus || !selectedVM) return
|
||||
if (liveVMStatus.vmid !== selectedVM.vmid) return
|
||||
setSelectedVM((prev) => (prev ? { ...prev, ...liveVMStatus } : prev))
|
||||
}, [liveVMStatus])
|
||||
|
||||
const handleVMClick = async (vm: VMData) => {
|
||||
setSelectedVM(vm)
|
||||
setCurrentView("main")
|
||||
|
||||
Reference in New Issue
Block a user