Clipboard fix: fallback fuer HTTP (non-secure) context, SSH copy-to-clipboard
This commit is contained in:
parent
025840e9ba
commit
983ecf390c
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import api from '../api/client'
|
import api from '../api/client'
|
||||||
import StatusBadge from './StatusBadge'
|
import StatusBadge from './StatusBadge'
|
||||||
|
import { copyToClipboard } from '../utils/clipboard'
|
||||||
import {
|
import {
|
||||||
X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route,
|
X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route,
|
||||||
Globe, Key, Download, Terminal, Wifi, Search, Plus, Trash2, Copy, Check, ExternalLink, Unplug, Plug, MonitorSmartphone,
|
Globe, Key, Download, Terminal, Wifi, Search, Plus, Trash2, Copy, Check, ExternalLink, Unplug, Plug, MonitorSmartphone,
|
||||||
@ -565,6 +566,7 @@ function TunnelTab({ agentId, agent }) {
|
|||||||
const [customHost, setCustomHost] = useState('127.0.0.1')
|
const [customHost, setCustomHost] = useState('127.0.0.1')
|
||||||
const [customPort, setCustomPort] = useState('')
|
const [customPort, setCustomPort] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const [sshCopied, setSshCopied] = useState(null)
|
||||||
|
|
||||||
const backendHost = '192.168.85.13'
|
const backendHost = '192.168.85.13'
|
||||||
|
|
||||||
@ -594,10 +596,17 @@ function TunnelTab({ agentId, agent }) {
|
|||||||
const data = resp?.data || resp
|
const data = resp?.data || resp
|
||||||
// Reload tunnel list
|
// Reload tunnel list
|
||||||
loadTunnels()
|
loadTunnels()
|
||||||
// If WebGUI, open in new tab
|
// Auto-open for known services
|
||||||
if (targetPort === 4444 && data?.proxy_port) {
|
if (data?.proxy_port) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (targetPort === 4444) {
|
||||||
window.open(`https://${backendHost}:${data.proxy_port}`, '_blank')
|
window.open(`https://${backendHost}:${data.proxy_port}`, '_blank')
|
||||||
|
} else if (targetPort === 22) {
|
||||||
|
const cmd = `ssh root@${backendHost} -p ${data.proxy_port}`
|
||||||
|
copyToClipboard(cmd)
|
||||||
|
setSshCopied(data.proxy_port)
|
||||||
|
setTimeout(() => setSshCopied(null), 4000)
|
||||||
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
@ -687,6 +696,12 @@ function TunnelTab({ agentId, agent }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{sshCopied && (
|
||||||
|
<div className="text-sm text-green-400 bg-green-900/20 rounded px-3 py-2 border border-green-800/50 flex items-center gap-2">
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
SSH-Befehl kopiert — in CMD/Terminal einfuegen und ausfuehren
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{error && <div className="text-sm text-red-400 bg-red-900/20 rounded px-3 py-2 border border-red-800/50">{error}</div>}
|
{error && <div className="text-sm text-red-400 bg-red-900/20 rounded px-3 py-2 border border-red-800/50">{error}</div>}
|
||||||
|
|
||||||
{/* Active Tunnels */}
|
{/* Active Tunnels */}
|
||||||
@ -719,6 +734,7 @@ function TunnelTab({ agentId, agent }) {
|
|||||||
: t.target || '—'
|
: t.target || '—'
|
||||||
const proxyUrl = proxyPort ? `${backendHost}:${proxyPort}` : '—'
|
const proxyUrl = proxyPort ? `${backendHost}:${proxyPort}` : '—'
|
||||||
const isWebGUI = t.target_port === 4444 || t.target_port === '4444'
|
const isWebGUI = t.target_port === 4444 || t.target_port === '4444'
|
||||||
|
const isSSH = t.target_port === 22 || t.target_port === '22'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={tid}>
|
<tr key={tid}>
|
||||||
@ -737,6 +753,25 @@ function TunnelTab({ agentId, agent }) {
|
|||||||
<ExternalLink className="w-3.5 h-3.5" />
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
{isSSH && proxyPort && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const cmd = `ssh root@${backendHost} -p ${proxyPort}`
|
||||||
|
copyToClipboard(cmd)
|
||||||
|
setSshCopied(proxyPort)
|
||||||
|
setTimeout(() => setSshCopied(null), 3000)
|
||||||
|
}}
|
||||||
|
className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded transition-colors ${
|
||||||
|
sshCopied === proxyPort
|
||||||
|
? 'bg-green-900/30 text-green-400 border border-green-700/50'
|
||||||
|
: 'bg-blue-900/30 text-blue-400 border border-blue-700/50 hover:bg-blue-900/50'
|
||||||
|
}`}
|
||||||
|
title={`ssh root@${backendHost} -p ${proxyPort}`}
|
||||||
|
>
|
||||||
|
<Terminal className="w-3 h-3" />
|
||||||
|
{sshCopied === proxyPort ? 'Kopiert!' : 'SSH kopieren'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2.5">
|
<td className="px-3 py-2.5">
|
||||||
@ -832,7 +867,7 @@ function WireGuardTab({ agentId, sys }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyConfig = (text) => {
|
const copyConfig = (text) => {
|
||||||
navigator.clipboard.writeText(text)
|
copyToClipboard(text)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Copy, Check, Terminal, Monitor } from 'lucide-react'
|
import { Copy, Check, Terminal, Monitor } from 'lucide-react'
|
||||||
|
import { copyToClipboard } from '../utils/clipboard'
|
||||||
|
|
||||||
const BACKEND_URL = 'https://dsbmueller.spdns.org:8443'
|
const BACKEND_URL = 'https://dsbmueller.spdns.org:8443'
|
||||||
const API_KEY = '01532e5a7c9e70bf2757df77a2f5b9b9'
|
const API_KEY = '01532e5a7c9e70bf2757df77a2f5b9b9'
|
||||||
@ -9,7 +10,7 @@ export default function InstallGuide({ agentName, platform: initialPlatform }) {
|
|||||||
const [copied, setCopied] = useState(null)
|
const [copied, setCopied] = useState(null)
|
||||||
|
|
||||||
const copyText = (text, id) => {
|
const copyText = (text, id) => {
|
||||||
navigator.clipboard.writeText(text)
|
copyToClipboard(text)
|
||||||
setCopied(id)
|
setCopied(id)
|
||||||
setTimeout(() => setCopied(null), 2000)
|
setTimeout(() => setCopied(null), 2000)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -267,23 +267,23 @@ export default function Agents() {
|
|||||||
<div className="text-gray-500">Laden...</div>
|
<div className="text-gray-500">Laden...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-gray-900 rounded-lg border border-gray-800 overflow-x-auto">
|
<div className="bg-gray-900 rounded-lg border border-gray-800 overflow-x-auto">
|
||||||
<table className="w-full text-sm whitespace-nowrap">
|
<table className="w-full text-xs whitespace-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-800 text-left text-gray-500 text-xs">
|
<tr className="border-b border-gray-800 text-left text-gray-500 text-xs">
|
||||||
<th className="px-3 py-2 font-medium w-8"></th>
|
<th className="px-2 py-1.5 font-medium w-8"></th>
|
||||||
{isColVisible('customer') && <SortHeader label="KUNDE" k="customer" />}
|
{isColVisible('customer') && <SortHeader label="KUNDE" k="customer" />}
|
||||||
{isColVisible('name') && <SortHeader label="NAME" k="name" />}
|
{isColVisible('name') && <SortHeader label="NAME" k="name" />}
|
||||||
{isColVisible('hostname') && <th className="px-3 py-2 font-medium">HOSTNAME</th>}
|
{isColVisible('hostname') && <th className="px-2 py-1.5 font-medium">HOSTNAME</th>}
|
||||||
{isColVisible('version') && <SortHeader label="VERSION" k="version" />}
|
{isColVisible('version') && <SortHeader label="VERSION" k="version" />}
|
||||||
{isColVisible('wanip') && <th className="px-3 py-2 font-medium">WAN IP</th>}
|
{isColVisible('wanip') && <th className="px-2 py-1.5 font-medium">WAN IP</th>}
|
||||||
{isColVisible('lanip') && <th className="px-3 py-2 font-medium">LAN IP</th>}
|
{isColVisible('lanip') && <th className="px-2 py-1.5 font-medium">LAN IP</th>}
|
||||||
{isColVisible('uptime') && <SortHeader label="UPTIME" k="uptime" />}
|
{isColVisible('uptime') && <SortHeader label="UPTIME" k="uptime" />}
|
||||||
{isColVisible('cpu') && <SortHeader label="CPU" k="cpu" />}
|
{isColVisible('cpu') && <SortHeader label="CPU" k="cpu" />}
|
||||||
{isColVisible('ram') && <SortHeader label="RAM" k="ram" />}
|
{isColVisible('ram') && <SortHeader label="RAM" k="ram" />}
|
||||||
{isColVisible('disk') && <SortHeader label="DISK" k="disk" />}
|
{isColVisible('disk') && <SortHeader label="DISK" k="disk" />}
|
||||||
{isColVisible('lastresponse') && <th className="px-3 py-2 font-medium">LETZTER KONTAKT</th>}
|
{isColVisible('lastresponse') && <th className="px-2 py-1.5 font-medium">LETZTER KONTAKT</th>}
|
||||||
{isColVisible('gateways') && <th className="px-3 py-2 font-medium">GATEWAYS</th>}
|
{isColVisible('gateways') && <th className="px-2 py-1.5 font-medium">GATEWAYS</th>}
|
||||||
{isColVisible('agentversion') && <th className="px-3 py-2 font-medium">AGENT</th>}
|
{isColVisible('agentversion') && <th className="px-2 py-1.5 font-medium">AGENT</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-800">
|
<tbody className="divide-y divide-gray-800">
|
||||||
@ -293,37 +293,30 @@ export default function Agents() {
|
|||||||
onClick={() => setSelectedId(a.id)}
|
onClick={() => setSelectedId(a.id)}
|
||||||
className={`hover:bg-gray-800/50 transition-colors cursor-pointer ${selectedId === a.id ? 'bg-gray-800/70' : ''}`}
|
className={`hover:bg-gray-800/50 transition-colors cursor-pointer ${selectedId === a.id ? 'bg-gray-800/70' : ''}`}
|
||||||
>
|
>
|
||||||
<td className="px-3 py-2"><StatusBadge status={a.status} size="dot" /></td>
|
<td className="px-2 py-1.5"><StatusBadge status={a.status} size="dot" /></td>
|
||||||
{isColVisible('customer') && (
|
{isColVisible('customer') && (
|
||||||
<td className="px-3 py-2 text-gray-400">
|
<td className="px-2 py-1.5 text-gray-400">
|
||||||
{a.customer ? <span className="text-orange-400">{a.customerNumber}</span> : <span className="text-gray-600">—</span>}
|
{a.customer ? <span className="text-orange-400">{a.customerNumber}</span> : <span className="text-gray-600">—</span>}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{isColVisible('name') && (
|
{isColVisible('name') && (
|
||||||
<td className="px-3 py-2 text-white font-medium">
|
<td className="px-2 py-1.5 text-white font-medium">{a.name}</td>
|
||||||
<span className="inline-flex items-center gap-2">
|
|
||||||
<span className={`text-[10px] font-mono px-1 rounded ${a.platform === 'linux' ? 'bg-blue-900/50 text-blue-400' : 'bg-orange-900/50 text-orange-400'}`}>
|
|
||||||
{a.platform === 'linux' ? 'LNX' : 'OPN'}
|
|
||||||
</span>
|
|
||||||
{a.name}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
)}
|
)}
|
||||||
{isColVisible('hostname') && <td className="px-3 py-2 text-gray-400 text-xs">{a.hostname}</td>}
|
{isColVisible('hostname') && <td className="px-2 py-1.5 text-gray-400 text-xs">{a.hostname}</td>}
|
||||||
{isColVisible('version') && <td className="px-3 py-2 text-gray-400">{a.version || '—'}</td>}
|
{isColVisible('version') && <td className="px-2 py-1.5 text-gray-400">{a.version || '—'}</td>}
|
||||||
{isColVisible('wanip') && <td className="px-3 py-2 text-gray-400">{a.wanIp || '—'}</td>}
|
{isColVisible('wanip') && <td className="px-2 py-1.5 text-gray-400">{a.wanIp || '—'}</td>}
|
||||||
{isColVisible('lanip') && <td className="px-3 py-2 text-gray-400">{a.lanIp || '—'}</td>}
|
{isColVisible('lanip') && <td className="px-2 py-1.5 text-gray-400">{a.lanIp || '—'}</td>}
|
||||||
{isColVisible('uptime') && <td className="px-3 py-2 text-gray-400">{a.uptime ? formatUptime(a.uptime) : '—'}</td>}
|
{isColVisible('uptime') && <td className="px-2 py-1.5 text-gray-400">{a.uptime ? formatUptime(a.uptime) : '—'}</td>}
|
||||||
{isColVisible('cpu') && <td className="px-3 py-2"><MiniBar value={a.cpuPct} /></td>}
|
{isColVisible('cpu') && <td className="px-2 py-1.5"><MiniBar value={a.cpuPct} /></td>}
|
||||||
{isColVisible('ram') && <td className="px-3 py-2"><MiniBar value={a.ramPct} /></td>}
|
{isColVisible('ram') && <td className="px-2 py-1.5"><MiniBar value={a.ramPct} /></td>}
|
||||||
{isColVisible('disk') && <td className="px-3 py-2"><MiniBar value={a.diskPct} /></td>}
|
{isColVisible('disk') && <td className="px-2 py-1.5"><MiniBar value={a.diskPct} /></td>}
|
||||||
{isColVisible('lastresponse') && (
|
{isColVisible('lastresponse') && (
|
||||||
<td className="px-3 py-2 text-gray-500 text-xs">
|
<td className="px-2 py-1.5 text-gray-500 text-xs">
|
||||||
{a.last_heartbeat ? new Date(a.last_heartbeat).toLocaleString('de-DE') : '—'}
|
{a.last_heartbeat ? new Date(a.last_heartbeat).toLocaleString('de-DE') : '—'}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{isColVisible('gateways') && (
|
{isColVisible('gateways') && (
|
||||||
<td className="px-3 py-2">
|
<td className="px-2 py-1.5">
|
||||||
{a.gateways.length > 0 ? (
|
{a.gateways.length > 0 ? (
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
{a.gateways.map((gw) => (
|
{a.gateways.map((gw) => (
|
||||||
@ -337,7 +330,7 @@ export default function Agents() {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{isColVisible('agentversion') && (
|
{isColVisible('agentversion') && (
|
||||||
<td className="px-3 py-2 text-gray-500 text-xs">{a.agent_version || '—'}</td>
|
<td className="px-2 py-1.5 text-gray-500 text-xs">{a.agent_version || '—'}</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@ -463,14 +456,14 @@ function gwDotColor(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MiniBar({ value }) {
|
function MiniBar({ value }) {
|
||||||
if (value === null || value === undefined) return <span className="text-gray-600 text-xs">—</span>
|
if (value === null || value === undefined) return <span className="text-gray-600 text-[10px]">—</span>
|
||||||
const color = value > 90 ? 'bg-red-500' : value > 70 ? 'bg-yellow-500' : value > 50 ? 'bg-blue-500' : 'bg-green-500'
|
const color = value > 90 ? 'bg-red-500' : value > 70 ? 'bg-yellow-500' : value > 50 ? 'bg-blue-500' : 'bg-green-500'
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1">
|
||||||
<div className="w-12 h-1.5 bg-gray-800 rounded overflow-hidden">
|
<div className="w-10 h-1 bg-gray-800 rounded overflow-hidden">
|
||||||
<div className={`h-full rounded ${color}`} style={{ width: `${Math.min(value, 100)}%` }} />
|
<div className={`h-full rounded ${color}`} style={{ width: `${Math.min(value, 100)}%` }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-400">{value.toFixed(0)}%</span>
|
<span className="text-[10px] text-gray-400">{value.toFixed(0)}%</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef } from 'react'
|
||||||
import api from '../api/client'
|
import api from '../api/client'
|
||||||
import { Upload, RefreshCw, CheckCircle, AlertTriangle, ArrowUpCircle, X, Monitor, Server, Cpu, Package, Copy, Check } from 'lucide-react'
|
import { Upload, RefreshCw, CheckCircle, AlertTriangle, ArrowUpCircle, X, Monitor, Server, Cpu, Package, Copy, Check } from 'lucide-react'
|
||||||
|
import { copyToClipboard } from '../utils/clipboard'
|
||||||
|
|
||||||
const PLATFORMS = [
|
const PLATFORMS = [
|
||||||
{ id: 'freebsd', label: 'FreeBSD / OPNsense', icon: Server },
|
{ id: 'freebsd', label: 'FreeBSD / OPNsense', icon: Server },
|
||||||
@ -245,7 +246,7 @@ export default function Firmware() {
|
|||||||
<pre className="text-xs font-mono text-orange-300 whitespace-pre-wrap">{`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`}</pre>
|
<pre className="text-xs font-mono text-orange-300 whitespace-pre-wrap">{`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`}</pre>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`)
|
copyToClipboard(`fetch --no-verify-peer --no-verify-hostname -o /tmp/installer.zip "${BACKEND_URL}/api/v1/installer/download?platform=freebsd&api_key=01532e5a7c9e70bf2757df77a2f5b9b9" && cd /tmp && unzip -o installer.zip && sh /tmp/install.sh`)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import api from '../api/client'
|
import api from '../api/client'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { copyToClipboard } from '../utils/clipboard'
|
||||||
import { Plus, Trash2, KeyRound, ShieldAlert, Copy, Check, Key } from 'lucide-react'
|
import { Plus, Trash2, KeyRound, ShieldAlert, Copy, Check, Key } from 'lucide-react'
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@ -256,8 +257,8 @@ function APIKeysSection() {
|
|||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyToClipboard = (text, id) => {
|
const copyText = (text, id) => {
|
||||||
navigator.clipboard.writeText(text)
|
copyToClipboard(text)
|
||||||
setCopied(id)
|
setCopied(id)
|
||||||
setTimeout(() => setCopied(null), 2000)
|
setTimeout(() => setCopied(null), 2000)
|
||||||
}
|
}
|
||||||
@ -312,7 +313,7 @@ function APIKeysSection() {
|
|||||||
{newKey.key}
|
{newKey.key}
|
||||||
</code>
|
</code>
|
||||||
<button
|
<button
|
||||||
onClick={() => copyToClipboard(newKey.key, 'new')}
|
onClick={() => copyText(newKey.key, 'new')}
|
||||||
className="px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded text-gray-300 transition-colors"
|
className="px-3 py-2 bg-gray-800 hover:bg-gray-700 rounded text-gray-300 transition-colors"
|
||||||
>
|
>
|
||||||
{copied === 'new' ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
|
{copied === 'new' ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
|
||||||
@ -343,7 +344,7 @@ function APIKeysSection() {
|
|||||||
<div className="flex items-center gap-3 mt-0.5">
|
<div className="flex items-center gap-3 mt-0.5">
|
||||||
<span className="text-xs text-gray-600 font-mono">{k.key.substring(0, 8)}...{k.key.substring(k.key.length - 4)}</span>
|
<span className="text-xs text-gray-600 font-mono">{k.key.substring(0, 8)}...{k.key.substring(k.key.length - 4)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => copyToClipboard(k.key, k.id)}
|
onClick={() => copyText(k.key, k.id)}
|
||||||
className="text-gray-600 hover:text-orange-400 transition-colors"
|
className="text-gray-600 hover:text-orange-400 transition-colors"
|
||||||
title="Key kopieren"
|
title="Key kopieren"
|
||||||
>
|
>
|
||||||
|
|||||||
25
frontend/src/utils/clipboard.js
Normal file
25
frontend/src/utils/clipboard.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copy text to clipboard with fallback for HTTP (non-secure) contexts.
|
||||||
|
* navigator.clipboard.writeText requires HTTPS or localhost.
|
||||||
|
*/
|
||||||
|
export function copyToClipboard(text) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
return navigator.clipboard.writeText(text)
|
||||||
|
}
|
||||||
|
// Fallback: hidden textarea + execCommand
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.left = '-9999px'
|
||||||
|
textarea.style.top = '-9999px'
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.focus()
|
||||||
|
textarea.select()
|
||||||
|
try {
|
||||||
|
document.execCommand('copy')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Copy failed:', e)
|
||||||
|
}
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user