mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2024-11-22 07:10:09 +01:00
Added OpenStreetMap for Ping and Traceroute
This commit is contained in:
parent
7fe4889b6e
commit
ff0147bebb
179
src/dashboard.py
179
src/dashboard.py
@ -1781,73 +1781,95 @@ def API_allowAccessPeers(configName: str) -> ResponseObject:
|
||||
|
||||
@app.post(f'{APP_PREFIX}/api/addPeers/<configName>')
|
||||
def API_addPeers(configName):
|
||||
data: dict = request.get_json()
|
||||
bulkAdd = data['bulkAdd']
|
||||
bulkAddAmount = data['bulkAddAmount']
|
||||
public_key = data['public_key']
|
||||
allowed_ips = data['allowed_ips']
|
||||
endpoint_allowed_ip = data['endpoint_allowed_ip']
|
||||
dns_addresses = data['DNS']
|
||||
mtu = data['mtu']
|
||||
keep_alive = data['keepalive']
|
||||
preshared_key = data['preshared_key']
|
||||
preshared_key_bulkAdd: bool = data['preshared_key_bulkAdd']
|
||||
|
||||
if configName in WireguardConfigurations.keys():
|
||||
config = WireguardConfigurations.get(configName)
|
||||
if (not bulkAdd and (len(public_key) == 0 or len(allowed_ips) == 0)) or len(endpoint_allowed_ip) == 0:
|
||||
return ResponseObject(False, "Please fill in all required box")
|
||||
if not config.getStatus():
|
||||
config.toggleConfiguration()
|
||||
try:
|
||||
data: dict = request.get_json()
|
||||
|
||||
availableIps = _getWireguardConfigurationAvailableIP(configName)
|
||||
|
||||
if bulkAdd:
|
||||
if bulkAddAmount < 1:
|
||||
return ResponseObject(False, "Please specify amount of peers you want to add")
|
||||
if not availableIps[0]:
|
||||
return ResponseObject(False, "No more available IP can assign")
|
||||
if bulkAddAmount > len(availableIps[1]):
|
||||
return ResponseObject(False,
|
||||
f"The maximum number of peers can add is {len(availableIps[1])}")
|
||||
|
||||
keyPairs = []
|
||||
for i in range(bulkAddAmount):
|
||||
newPrivateKey = _generatePrivateKey()[1]
|
||||
keyPairs.append({
|
||||
"private_key": newPrivateKey,
|
||||
"id": _generatePublicKey(newPrivateKey)[1],
|
||||
"preshared_key": (_generatePrivateKey()[1] if preshared_key_bulkAdd else ""),
|
||||
"allowed_ip": availableIps[1][i],
|
||||
"name": f"BulkPeer #{(i + 1)}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
})
|
||||
if len(keyPairs) == 0:
|
||||
return ResponseObject(False, "Generating key pairs by bulk failed")
|
||||
config.addPeers(keyPairs)
|
||||
bulkAdd: bool = data.get("bulkAdd", False)
|
||||
bulkAddAmount: int = data.get('bulkAddAmount', 0)
|
||||
preshared_key_bulkAdd: bool = data.get('preshared_key_bulkAdd', False)
|
||||
|
||||
|
||||
public_key: str = data.get('public_key', "")
|
||||
allowed_ips: list[str] = data.get('allowed_ips', "")
|
||||
|
||||
for kp in keyPairs:
|
||||
found, peer = config.searchPeer(kp['id'])
|
||||
endpoint_allowed_ip: str = data.get('endpoint_allowed_ip', DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1])
|
||||
dns_addresses: str = data.get('DNS', DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1])
|
||||
mtu: int = data.get('mtu', int(DashboardConfig.GetConfig("Peers", "peer_MTU")[1]))
|
||||
keep_alive: int = data.get('keepalive', int(DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1]))
|
||||
preshared_key: str = data.get('preshared_key', "")
|
||||
|
||||
|
||||
if type(mtu) is not int or mtu < 0 or mtu > 1460:
|
||||
mtu = int(DashboardConfig.GetConfig("Peers", "peer_MTU")[1])
|
||||
if type(keep_alive) is not int or keep_alive < 0:
|
||||
keep_alive = int(DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1])
|
||||
if len(dns_addresses) == 0:
|
||||
dns_addresses = DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1]
|
||||
if len(endpoint_allowed_ip) == 0:
|
||||
endpoint_allowed_ip = DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1]
|
||||
|
||||
|
||||
config = WireguardConfigurations.get(configName)
|
||||
if not bulkAdd and (len(public_key) == 0 or len(allowed_ips) == 0):
|
||||
return ResponseObject(False, "Please provide at lease public_key and allowed_ips")
|
||||
if not config.getStatus():
|
||||
config.toggleConfiguration()
|
||||
|
||||
availableIps = _getWireguardConfigurationAvailableIP(configName)
|
||||
|
||||
if bulkAdd:
|
||||
if type(preshared_key_bulkAdd) is not bool:
|
||||
preshared_key_bulkAdd = False
|
||||
|
||||
if type(bulkAddAmount) is not int or bulkAddAmount < 1:
|
||||
return ResponseObject(False, "Please specify amount of peers you want to add")
|
||||
if not availableIps[0]:
|
||||
return ResponseObject(False, "No more available IP can assign")
|
||||
if bulkAddAmount > len(availableIps[1]):
|
||||
return ResponseObject(False,
|
||||
f"The maximum number of peers can add is {len(availableIps[1])}")
|
||||
|
||||
keyPairs = []
|
||||
for i in range(bulkAddAmount):
|
||||
newPrivateKey = _generatePrivateKey()[1]
|
||||
keyPairs.append({
|
||||
"private_key": newPrivateKey,
|
||||
"id": _generatePublicKey(newPrivateKey)[1],
|
||||
"preshared_key": (_generatePrivateKey()[1] if preshared_key_bulkAdd else ""),
|
||||
"allowed_ip": availableIps[1][i],
|
||||
"name": f"BulkPeer #{(i + 1)}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
})
|
||||
if len(keyPairs) == 0:
|
||||
return ResponseObject(False, "Generating key pairs by bulk failed")
|
||||
config.addPeers(keyPairs)
|
||||
|
||||
for kp in keyPairs:
|
||||
found, peer = config.searchPeer(kp['id'])
|
||||
if found:
|
||||
if not peer.updatePeer(kp['name'], kp['private_key'], kp['preshared_key'], dns_addresses,
|
||||
kp['allowed_ip'], endpoint_allowed_ip, mtu, keep_alive):
|
||||
return ResponseObject(False, "Failed to add peers in bulk")
|
||||
return ResponseObject()
|
||||
|
||||
else:
|
||||
if config.searchPeer(public_key)[0] is True:
|
||||
return ResponseObject(False, f"This peer already exist")
|
||||
name = data.get("name", "")
|
||||
private_key = data.get("private_key", "")
|
||||
|
||||
for i in allowed_ips:
|
||||
if i not in availableIps[1]:
|
||||
return ResponseObject(False, f"This IP is not available: {i}")
|
||||
|
||||
config.addPeers([{"id": public_key, "allowed_ip": ','.join(allowed_ips)}])
|
||||
found, peer = config.searchPeer(public_key)
|
||||
if found:
|
||||
if not peer.updatePeer(kp['name'], kp['private_key'], kp['preshared_key'], dns_addresses,
|
||||
kp['allowed_ip'], endpoint_allowed_ip, mtu, keep_alive):
|
||||
return ResponseObject(False, "Failed to add peers in bulk")
|
||||
return ResponseObject()
|
||||
|
||||
else:
|
||||
if config.searchPeer(public_key)[0] is True:
|
||||
return ResponseObject(False, f"This peer already exist")
|
||||
name = data['name']
|
||||
private_key = data['private_key']
|
||||
|
||||
for i in allowed_ips:
|
||||
if i not in availableIps[1]:
|
||||
return ResponseObject(False, f"This IP is not available: {i}")
|
||||
|
||||
config.addPeers([{"id": public_key, "allowed_ip": ','.join(allowed_ips)}])
|
||||
found, peer = config.searchPeer(public_key)
|
||||
if found:
|
||||
return peer.updatePeer(name, private_key, preshared_key, dns_addresses, ",".join(allowed_ips),
|
||||
endpoint_allowed_ip, mtu, keep_alive)
|
||||
return peer.updatePeer(name, private_key, preshared_key, dns_addresses, ",".join(allowed_ips),
|
||||
endpoint_allowed_ip, mtu, keep_alive)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return ResponseObject(False, "Add peers failed. Please see data for specific issue")
|
||||
|
||||
return ResponseObject(False, "Configuration does not exist")
|
||||
|
||||
@ -1992,6 +2014,7 @@ def API_ping_getAllPeersIpAddress():
|
||||
ips[c.Name] = cips
|
||||
return ResponseObject(data=ips)
|
||||
|
||||
import requests
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/ping/execute')
|
||||
def API_ping_execute():
|
||||
@ -2001,8 +2024,8 @@ def API_ping_execute():
|
||||
try:
|
||||
if ip is not None and len(ip) > 0 and count is not None and count.isnumeric():
|
||||
result = ping(ip, count=int(count), source=None)
|
||||
|
||||
return ResponseObject(data={
|
||||
|
||||
data = {
|
||||
"address": result.address,
|
||||
"is_alive": result.is_alive,
|
||||
"min_rtt": result.min_rtt,
|
||||
@ -2010,9 +2033,17 @@ def API_ping_execute():
|
||||
"max_rtt": result.max_rtt,
|
||||
"package_sent": result.packets_sent,
|
||||
"package_received": result.packets_received,
|
||||
"package_loss": result.packet_loss
|
||||
})
|
||||
|
||||
"package_loss": result.packet_loss,
|
||||
"geo": None
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
r = requests.get(f"http://ip-api.com/json/{result.address}?field=city")
|
||||
data['geo'] = r.json()
|
||||
except Exception as e:
|
||||
pass
|
||||
return ResponseObject(data=data)
|
||||
return ResponseObject(False, "Please specify an IP Address (v4/v6)")
|
||||
except Exception as exp:
|
||||
return ResponseObject(False, exp)
|
||||
@ -2024,7 +2055,7 @@ def API_traceroute_execute():
|
||||
if "ipAddress" in request.args.keys() and len(request.args.get("ipAddress")) > 0:
|
||||
ipAddress = request.args.get('ipAddress')
|
||||
try:
|
||||
tracerouteResult = traceroute(ipAddress)
|
||||
tracerouteResult = traceroute(ipAddress, timeout=1, max_hops=64)
|
||||
result = []
|
||||
for hop in tracerouteResult:
|
||||
if len(result) > 1:
|
||||
@ -2049,6 +2080,15 @@ def API_traceroute_execute():
|
||||
"min_rtt": hop.min_rtt,
|
||||
"max_rtt": hop.max_rtt
|
||||
})
|
||||
try:
|
||||
r = requests.post(f"http://ip-api.com/batch?fields=city,country,lat,lon,query",
|
||||
data=json.dumps([x['ip'] for x in result]))
|
||||
d = r.json()
|
||||
for i in range(len(result)):
|
||||
result[i]['geo'] = d[i]
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return ResponseObject(data=result)
|
||||
except Exception as exp:
|
||||
return ResponseObject(False, exp)
|
||||
@ -2077,7 +2117,6 @@ def API_getDashboardUpdate():
|
||||
Sign Up
|
||||
'''
|
||||
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/isTotpEnabled')
|
||||
def API_isTotpEnabled():
|
||||
return (
|
||||
|
@ -5,4 +5,5 @@ pyotp
|
||||
Flask
|
||||
flask-cors
|
||||
icmplib
|
||||
gunicorn
|
||||
gunicorn
|
||||
requests
|
164
src/static/app/package-lock.json
generated
164
src/static/app/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "4.0.2",
|
||||
"version": "4.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "app",
|
||||
"version": "4.0.2",
|
||||
"version": "4.1.0",
|
||||
"dependencies": {
|
||||
"@vuepic/vue-datepicker": "^9.0.1",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
@ -21,6 +21,7 @@
|
||||
"i": "^0.3.7",
|
||||
"is-cidr": "^5.0.3",
|
||||
"npm": "^10.5.0",
|
||||
"ol": "^10.2.1",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcodejs": "^1.0.0",
|
||||
@ -723,6 +724,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@petamoriken/float16": {
|
||||
"version": "3.8.7",
|
||||
"resolved": "https://registry.npmmirror.com/@petamoriken/float16/-/float16-3.8.7.tgz",
|
||||
"integrity": "sha512-/Ri4xDDpe12NT6Ex/DRgHzLlobiQXEW/hmG08w1wj/YU7hLemk97c+zHQFp0iZQ9r7YqgLEXZR2sls4HxBf9NA=="
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@ -964,6 +970,11 @@
|
||||
"xmlbuilder": ">=11.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/rbush": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/rbush/-/rbush-3.0.3.tgz",
|
||||
"integrity": "sha512-lX55lR0iYCgapxD3IrgujpQA1zDxwZI5qMRelKvmKAsSMplFVr7wmMpG7/6+Op2tjrgEex8o3vjg8CRDrRNYxg=="
|
||||
},
|
||||
"node_modules/@types/verror": {
|
||||
"version": "1.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz",
|
||||
@ -1717,6 +1728,36 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/color-parse": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/color-parse/-/color-parse-2.0.2.tgz",
|
||||
"integrity": "sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-parse/node_modules/color-name": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-2.0.0.tgz",
|
||||
"integrity": "sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-rgba": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/color-rgba/-/color-rgba-3.0.0.tgz",
|
||||
"integrity": "sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==",
|
||||
"dependencies": {
|
||||
"color-parse": "^2.0.0",
|
||||
"color-space": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-space": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-space/-/color-space-2.0.1.tgz",
|
||||
"integrity": "sha512-nKqUYlo0vZATVOFHY810BSYjmCARrG7e5R3UE3CQlyjJTvv5kSSmPG1kzm/oDyyqjehM+lW1RnEt9It9GNa5JA=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@ -2005,6 +2046,11 @@
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
|
||||
"integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA=="
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/earcut/-/earcut-3.0.0.tgz",
|
||||
"integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg=="
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@ -2358,6 +2404,24 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/geotiff": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/geotiff/-/geotiff-2.1.3.tgz",
|
||||
"integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==",
|
||||
"dependencies": {
|
||||
"@petamoriken/float16": "^3.4.7",
|
||||
"lerc": "^3.0.0",
|
||||
"pako": "^2.0.4",
|
||||
"parse-headers": "^2.0.2",
|
||||
"quick-lru": "^6.1.1",
|
||||
"web-worker": "^1.2.0",
|
||||
"xml-utils": "^1.0.2",
|
||||
"zstddec": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
@ -2731,6 +2795,11 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lerc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/lerc/-/lerc-3.0.0.tgz",
|
||||
"integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww=="
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@ -5476,6 +5545,24 @@
|
||||
"inBundle": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ol": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/ol/-/ol-10.2.1.tgz",
|
||||
"integrity": "sha512-2bB/y2vEnmzjqynP0NA7Cp8k86No3Psn63Dueicep3E3i09axWRVIG5IS/bylEAGfWQx0QXD/uljkyFoY60Wig==",
|
||||
"dependencies": {
|
||||
"@types/rbush": "3.0.3",
|
||||
"color-rgba": "^3.0.0",
|
||||
"color-space": "^2.0.1",
|
||||
"earcut": "^3.0.0",
|
||||
"geotiff": "^2.0.7",
|
||||
"pbf": "4.0.1",
|
||||
"rbush": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/openlayers"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@ -5522,6 +5609,16 @@
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
|
||||
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||
},
|
||||
"node_modules/parse-headers": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/parse-headers/-/parse-headers-2.0.5.tgz",
|
||||
"integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA=="
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@ -5566,6 +5663,17 @@
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
@ -5687,6 +5795,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -5717,6 +5830,30 @@
|
||||
"resolved": "https://registry.npmjs.org/qrcodejs/-/qrcodejs-1.0.0.tgz",
|
||||
"integrity": "sha512-67rj3mMBhSBepaD57qENnltO+r8rSYlqM7HGThks/BiyDAkc86sLvkKqjkqPS5v13f7tvnt6dbEf3qt7zq+BCg=="
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/quick-lru/-/quick-lru-6.1.2.tgz",
|
||||
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
|
||||
},
|
||||
"node_modules/rbush": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/rbush/-/rbush-4.0.1.tgz",
|
||||
"integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==",
|
||||
"dependencies": {
|
||||
"quickselect": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-config-file": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
|
||||
@ -5769,6 +5906,14 @@
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
@ -6264,6 +6409,11 @@
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-worker": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/web-worker/-/web-worker-1.3.0.tgz",
|
||||
"integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA=="
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@ -6318,6 +6468,11 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/xml-utils": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmmirror.com/xml-utils/-/xml-utils-1.10.1.tgz",
|
||||
"integrity": "sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ=="
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||
@ -6403,6 +6558,11 @@
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/zstddec": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/zstddec/-/zstddec-0.1.0.tgz",
|
||||
"integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
"i": "^0.3.7",
|
||||
"is-cidr": "^5.0.3",
|
||||
"npm": "^10.5.0",
|
||||
"ol": "^10.2.1",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcodejs": "^1.0.0",
|
||||
|
@ -19,6 +19,7 @@ export default {
|
||||
<input type="number" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.mtu"
|
||||
min="0"
|
||||
id="peer_mtu">
|
||||
</div>
|
||||
</template>
|
||||
|
@ -26,7 +26,7 @@ export default {
|
||||
return{
|
||||
data: {
|
||||
bulkAdd: false,
|
||||
bulkAddAmount: "",
|
||||
bulkAddAmount: 0,
|
||||
name: "",
|
||||
allowed_ips: [],
|
||||
private_key: "",
|
||||
|
113
src/static/app/src/components/map/map.vue
Normal file
113
src/static/app/src/components/map/map.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<script>
|
||||
import "ol/ol.css"
|
||||
import Map from 'ol/Map.js';
|
||||
import OSM from 'ol/source/OSM.js';
|
||||
import TileLayer from 'ol/layer/Tile.js';
|
||||
import View from 'ol/View.js';
|
||||
import {Feature} from "ol";
|
||||
import {fromLonLat} from "ol/proj"
|
||||
import {LineString, Point} from "ol/geom"
|
||||
import {Circle, Fill, Stroke, Style, Text} from "ol/style.js";
|
||||
import {Vector} from "ol/layer"
|
||||
import {Vector as SourceVector} from "ol/source"
|
||||
|
||||
export default {
|
||||
name: "map",
|
||||
props: {
|
||||
type: "",
|
||||
d: Object || Array
|
||||
},
|
||||
methods: {
|
||||
getLastLonLat(){
|
||||
if (this.type === 'traceroute'){
|
||||
const k = this.d.findLast(data => data.geo && data.geo.lat && data.geo.lon)
|
||||
if (k){
|
||||
return [k.geo.lon, k.geo.lat]
|
||||
}
|
||||
return [0,0]
|
||||
}
|
||||
return [this.d.geo.lon, this.d.geo.lat]
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM(),
|
||||
}),
|
||||
],
|
||||
view: new View({
|
||||
center: fromLonLat(this.getLastLonLat()),
|
||||
zoom: this.type === 'traceroute' ? 3:10,
|
||||
}),
|
||||
});
|
||||
const coordinates = [];
|
||||
const vectorSource = new SourceVector();
|
||||
if (this.type === 'traceroute'){
|
||||
console.log(this.getLastLonLat())
|
||||
this.d.forEach(data => {
|
||||
if (data.geo && data.geo.lat && data.geo.lon) {
|
||||
const coordinate = fromLonLat([data.geo.lon, data.geo.lat]);
|
||||
coordinates.push(coordinate);
|
||||
const l = this.getLastLonLat();
|
||||
|
||||
console.log(data.geo.lon, data.geo.lat)
|
||||
console.log( data.geo.lon === l[0] && data.geo.lat === l[1])
|
||||
const marker = new Feature({
|
||||
geometry: new Point(coordinate),
|
||||
last: data.geo.lon === l[0] && data.geo.lat === l[1]
|
||||
});
|
||||
vectorSource.addFeature(marker);
|
||||
}
|
||||
})
|
||||
}else{
|
||||
const coordinate = fromLonLat([this.d.geo.lon, this.d.geo.lat])
|
||||
coordinates.push(coordinate);
|
||||
const marker = new Feature({
|
||||
geometry: new Point(coordinate)
|
||||
});
|
||||
// Add marker to the vector source
|
||||
vectorSource.addFeature(marker);
|
||||
}
|
||||
const lineString = new LineString(coordinates);
|
||||
const lineFeature = new Feature({
|
||||
geometry: lineString
|
||||
});
|
||||
vectorSource.addFeature(lineFeature);
|
||||
const vectorLayer = new Vector({
|
||||
source: vectorSource,
|
||||
style: function(feature) {
|
||||
if (feature.getGeometry().getType() === 'Point') {
|
||||
return new Style({
|
||||
image: new Circle({
|
||||
radius: 10,
|
||||
fill: new Fill({ color: feature.get("last") ? '#dc3545':'#0d6efd' }),
|
||||
stroke: new Stroke({ color: 'white', width: 5 }),
|
||||
})
|
||||
});
|
||||
} else if (feature.getGeometry().getType() === 'LineString') {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
color: '#0d6efd', // Line color
|
||||
width: 2 // Line width
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
map.addLayer(vectorLayer);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="map" class="w-100 rounded-3" style="height: 300px"></div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.ol-layer canvas{
|
||||
border-radius: var(--bs-border-radius-lg) !important;
|
||||
}
|
||||
</style>
|
@ -1,9 +1,12 @@
|
||||
<script>
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import Map from "@/components/map/map.vue";
|
||||
|
||||
export default {
|
||||
name: "ping",
|
||||
components: {Map, LocaleText},
|
||||
data(){
|
||||
return {
|
||||
loading: false,
|
||||
@ -67,9 +70,11 @@ export default {
|
||||
<div class="col-sm-4 d-flex gap-2 flex-column">
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="configuration">
|
||||
<small>Configuration</small></label>
|
||||
<small>
|
||||
<LocaleText t="Configuration"></LocaleText>
|
||||
</small></label>
|
||||
<select class="form-select" v-model="this.selectedConfiguration">
|
||||
<option disabled selected :value="undefined">Select a Configuration...</option>
|
||||
<option disabled selected :value="undefined"></option>
|
||||
<option :value="key" v-for="(val, key) in this.cips">
|
||||
{{key}}
|
||||
</option>
|
||||
@ -77,9 +82,11 @@ export default {
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="peer">
|
||||
<small>Peer</small></label>
|
||||
<small>
|
||||
<LocaleText t="Peer"></LocaleText>
|
||||
</small></label>
|
||||
<select id="peer" class="form-select" v-model="this.selectedPeer" :disabled="this.selectedConfiguration === undefined">
|
||||
<option disabled selected :value="undefined">Select a Peer...</option>
|
||||
<option disabled selected :value="undefined"></option>
|
||||
<option v-if="this.selectedConfiguration !== undefined" :value="key" v-for="(peer, key) in
|
||||
this.cips[this.selectedConfiguration]">
|
||||
{{key}}
|
||||
@ -88,9 +95,11 @@ export default {
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="ip">
|
||||
<small>IP Address</small></label>
|
||||
<small>
|
||||
<LocaleText t="IP Address"></LocaleText>
|
||||
</small></label>
|
||||
<select id="ip" class="form-select" v-model="this.selectedIp" :disabled="this.selectedPeer === undefined">
|
||||
<option disabled selected :value="undefined">Select a IP...</option>
|
||||
<option disabled selected :value="undefined"></option>
|
||||
<option
|
||||
v-if="this.selectedPeer !== undefined"
|
||||
v-for="ip in this.cips[this.selectedConfiguration][this.selectedPeer].allowed_ips">
|
||||
@ -98,17 +107,50 @@ export default {
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="flex-grow-1 border-top"></div>
|
||||
<small class="text-muted">
|
||||
<LocaleText t="OR"></LocaleText>
|
||||
</small>
|
||||
<div class="flex-grow-1 border-top"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="ipAddress">
|
||||
<small>
|
||||
<LocaleText t="Enter IP Address / Hostname"></LocaleText>
|
||||
</small></label>
|
||||
<input class="form-control" type="text"
|
||||
id="ipAddress"
|
||||
v-model="this.selectedIp">
|
||||
</div>
|
||||
<div class="w-100 border-top my-2"></div>
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="count">
|
||||
<small>Ping Count</small></label>
|
||||
<input class="form-control" type="number"
|
||||
v-model="this.count"
|
||||
min="1" id="count" placeholder="How many times you want to ping?">
|
||||
<small>
|
||||
<LocaleText t="Count"></LocaleText>
|
||||
</small></label>
|
||||
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<button @click="this.count--"
|
||||
:disabled="this.count === 1"
|
||||
class="btn btn-sm bg-secondary-subtle text-secondary-emphasis">
|
||||
<i class="bi bi-dash-lg"></i>
|
||||
</button>
|
||||
<strong>{{this.count}}</strong>
|
||||
<button role="button" @click="this.count++" class="btn btn-sm bg-secondary-subtle text-secondary-emphasis">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- <input class="form-control" type="number" -->
|
||||
<!-- v-model="this.count"-->
|
||||
<!-- min="1" id="count" placeholder="How many times you want to ping?">-->
|
||||
</div>
|
||||
<button class="btn btn-primary rounded-3 mt-3"
|
||||
:disabled="!this.selectedIp"
|
||||
@click="this.execute()">
|
||||
<i class="bi bi-person-walking me-2"></i>Go!
|
||||
<i class="bi bi-person-walking me-2"></i>Ping!
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -123,9 +165,23 @@ export default {
|
||||
|
||||
<div v-else key="pingResult" class="d-flex flex-column gap-2 w-100">
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.15s">
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><small>Address</small></p>
|
||||
{{this.pingResult.address}}
|
||||
<div class="card-body row">
|
||||
<div class="col-sm">
|
||||
<p class="mb-0 text-muted">
|
||||
<small>
|
||||
<LocaleText t="IP Address"></LocaleText>
|
||||
</small>
|
||||
</p>
|
||||
{{this.pingResult.address}}
|
||||
</div>
|
||||
<div class="col-sm" v-if="this.pingResult.geo && this.pingResult.geo.status === 'success'">
|
||||
<p class="mb-0 text-muted">
|
||||
<small>
|
||||
<LocaleText t="Geolocation"></LocaleText>
|
||||
</small>
|
||||
</p>
|
||||
{{this.pingResult.geo.city}}, {{this.pingResult.geo.country}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.3s">
|
||||
@ -140,7 +196,9 @@ export default {
|
||||
</div>
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.45s">
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><small>Average / Min / Max Round Trip Time</small></p>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Average / Min / Max Round Trip Time"></LocaleText>
|
||||
</small></p>
|
||||
<samp>{{this.pingResult.avg_rtt}}ms /
|
||||
{{this.pingResult.min_rtt}}ms /
|
||||
{{this.pingResult.max_rtt}}ms
|
||||
@ -149,14 +207,16 @@ export default {
|
||||
</div>
|
||||
<div class="card rounded-3 bg-transparent shadow-sm animate__animated animate__fadeIn" style="animation-delay: 0.6s">
|
||||
<div class="card-body">
|
||||
<p class="mb-0 text-muted"><small>Sent / Received / Lost Package</small></p>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Sent / Received / Lost Package"></LocaleText>
|
||||
</small></p>
|
||||
<samp>{{this.pingResult.package_sent}} /
|
||||
{{this.pingResult.package_received}} /
|
||||
{{this.pingResult.package_loss}}
|
||||
</samp>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Map :d="this.pingResult" v-if="this.pingResult.geo && this.pingResult.geo.status === 'success'"></Map>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
<script>
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import Map from "@/components/map/map.vue";
|
||||
|
||||
export default {
|
||||
name: "traceroute",
|
||||
components: {Map},
|
||||
data(){
|
||||
return {
|
||||
tracing: false,
|
||||
@ -41,55 +43,75 @@ export default {
|
||||
<div class="mt-md-5 mt-3 text-body">
|
||||
<div class="container-md">
|
||||
<h3 class="mb-3 text-body">Traceroute</h3>
|
||||
<div class="row">
|
||||
<div class="col-sm-4 d-flex gap-2 flex-column">
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="ipAddress">
|
||||
<small>IP Address</small></label>
|
||||
<input
|
||||
id="ipAddress"
|
||||
class="form-control"
|
||||
v-model="this.ipAddress"
|
||||
type="text" placeholder="Enter an IP Address you want to trace :)">
|
||||
</div>
|
||||
<button class="btn btn-primary rounded-3 mt-3"
|
||||
:disabled="!this.store.regexCheckIP(this.ipAddress) || this.tracing"
|
||||
@click="this.execute()">
|
||||
<i class="bi bi-bullseye me-2"></i> {{this.tracing ? "Tracing...":"Trace It!"}}
|
||||
</button>
|
||||
<div class="d-flex gap-2 flex-column mb-5">
|
||||
<div>
|
||||
<label class="mb-1 text-muted" for="ipAddress">
|
||||
<small>IP Address</small></label>
|
||||
<input
|
||||
id="ipAddress"
|
||||
class="form-control"
|
||||
v-model="this.ipAddress"
|
||||
@keyup.enter="this.execute()"
|
||||
type="text" placeholder="Enter an IP Address you want to trace :)">
|
||||
</div>
|
||||
<div class="col-sm-8 position-relative">
|
||||
<TransitionGroup name="ping">
|
||||
<div v-if="!this.tracerouteResult" key="pingPlaceholder">
|
||||
<div class="pingPlaceholder bg-body-secondary rounded-3 mb-3"
|
||||
:class="{'animate__animated animate__flash animate__slower animate__infinite': this.tracing}"
|
||||
:style="{'animation-delay': `${x*0.05}s`}"
|
||||
v-for="x in 10" ></div>
|
||||
</div>
|
||||
<div v-else key="table" class="w-100">
|
||||
<table class="table table-borderless rounded-3 w-100">
|
||||
<button class="btn btn-primary rounded-3 mt-3"
|
||||
:disabled="this.tracing"
|
||||
@click="this.execute()">
|
||||
<i class="bi bi-bullseye me-2"></i> {{this.tracing ? "Tracing...":"Trace It!"}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="position-relative">
|
||||
<TransitionGroup name="ping">
|
||||
<div v-if="!this.tracerouteResult" key="pingPlaceholder">
|
||||
<div class="pingPlaceholder bg-body-secondary rounded-3 mb-3"
|
||||
:class="{'animate__animated animate__flash animate__slower animate__infinite': this.tracing}"
|
||||
:style="{'animation-delay': `${x*0.05}s`}"
|
||||
v-for="x in 5" ></div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Map :d="this.tracerouteResult" type="traceroute"></Map>
|
||||
|
||||
<div key="table" class="w-100 mt-2">
|
||||
<table class="table table-sm rounded-3 w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Hop</th>
|
||||
<th scope="col">IP Address</th>
|
||||
<th scope="col">Average / Min / Max Round Trip Time</th>
|
||||
<th scope="col">Average RTT (ms)</th>
|
||||
<th scope="col">Min RTT (ms)</th>
|
||||
<th scope="col">Max RTT (ms)</th>
|
||||
<th scope="col">Geolocation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(hop, key) in this.tracerouteResult"
|
||||
class="animate__fadeInUp animate__animated"
|
||||
:style="{'animation-delay': `${key * 0.05}s`}"
|
||||
>
|
||||
<td>{{hop.hop}}</td>
|
||||
<td>{{hop.ip}}</td>
|
||||
<td>{{hop.avg_rtt}} / {{hop.min_rtt}} / {{hop.max_rtt}}</td>
|
||||
</tr>
|
||||
<tr v-for="(hop, key) in this.tracerouteResult">
|
||||
<td>
|
||||
<small>{{hop.hop}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{hop.ip}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{hop.avg_rtt}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{hop.min_rtt}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{hop.max_rtt}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="hop.geo.city && hop.geo.country">
|
||||
<small>{{hop.geo.city}}, {{hop.geo.country}}</small>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -123,11 +145,7 @@ export default {
|
||||
}
|
||||
|
||||
table th, table td{
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
table tbody{
|
||||
border-top: 1em solid transparent;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > *{
|
||||
|
Loading…
Reference in New Issue
Block a user