mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-06 17:59:11 +00:00
gui use frontend-lib, fix memory leak (#467)
This commit is contained in:
@@ -1,296 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { getOsHostname } from '~/composables/network'
|
||||
|
||||
import { NetworkingMethod } from '~/types/network'
|
||||
|
||||
const props = defineProps<{
|
||||
configInvalid?: boolean
|
||||
instanceId?: string
|
||||
}>()
|
||||
|
||||
defineEmits(['runNetwork'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const networking_methods = ref([
|
||||
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
|
||||
{ value: NetworkingMethod.Manual, label: () => t('manual') },
|
||||
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
|
||||
])
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
const curNetwork = computed(() => {
|
||||
if (props.instanceId) {
|
||||
// console.log('instanceId', props.instanceId)
|
||||
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
|
||||
if (c !== undefined)
|
||||
return c
|
||||
}
|
||||
|
||||
return networkStore.curNetwork
|
||||
})
|
||||
|
||||
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
|
||||
|
||||
function searchUrlSuggestions(e: { query: string }): string[] {
|
||||
const query = e.query
|
||||
const ret = []
|
||||
// if query match "^\w+:.*", then no proto prefix
|
||||
if (query.match(/^\w+:.*/)) {
|
||||
// if query is a valid url, then add to suggestions
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(query)
|
||||
ret.push(query)
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
else {
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://${query}`
|
||||
// if query match ":\d+$", then no port suffix
|
||||
if (!query.match(/:\d+$/)) {
|
||||
item += `:${protos[proto]}`
|
||||
}
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
const publicServerSuggestions = ref([''])
|
||||
|
||||
function searchPresetPublicServers(e: { query: string }) {
|
||||
const presetPublicServers = [
|
||||
'tcp://public.easytier.top:11010',
|
||||
]
|
||||
|
||||
const query = e.query
|
||||
// if query is sub string of presetPublicServers, add to suggestions
|
||||
let ret = presetPublicServers.filter(item => item.includes(query))
|
||||
// add additional suggestions
|
||||
if (query.length > 0) {
|
||||
ret = ret.concat(searchUrlSuggestions(e))
|
||||
}
|
||||
|
||||
publicServerSuggestions.value = ret
|
||||
}
|
||||
|
||||
const peerSuggestions = ref([''])
|
||||
|
||||
function searchPeerSuggestions(e: { query: string }) {
|
||||
peerSuggestions.value = searchUrlSuggestions(e)
|
||||
}
|
||||
|
||||
const inetSuggestions = ref([''])
|
||||
|
||||
function searchInetSuggestions(e: { query: string }) {
|
||||
if (e.query.search('/') >= 0) {
|
||||
inetSuggestions.value = [e.query]
|
||||
} else {
|
||||
const ret = []
|
||||
for (let i = 0; i < 32; i++) {
|
||||
ret.push(`${e.query}/${i}`)
|
||||
}
|
||||
inetSuggestions.value = ret
|
||||
}
|
||||
}
|
||||
|
||||
const listenerSuggestions = ref([''])
|
||||
|
||||
function searchListenerSuggestiong(e: { query: string }) {
|
||||
const ret = []
|
||||
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://0.0.0.0:`
|
||||
// if query is a number, use it as port
|
||||
if (e.query.match(/^\d+$/)) {
|
||||
item += e.query
|
||||
}
|
||||
else {
|
||||
item += protos[proto]
|
||||
}
|
||||
|
||||
if (item.includes(e.query)) {
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (ret.length === 0) {
|
||||
ret.push(e.query)
|
||||
}
|
||||
|
||||
listenerSuggestions.value = ret
|
||||
}
|
||||
|
||||
function validateHostname() {
|
||||
if (curNetwork.value.hostname) {
|
||||
// eslint no-useless-escape
|
||||
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-z0-9\-]*/gi, '')
|
||||
if (name.length > 32)
|
||||
name = name.substring(0, 32)
|
||||
|
||||
if (curNetwork.value.hostname !== name)
|
||||
curNetwork.value.hostname = name
|
||||
}
|
||||
}
|
||||
|
||||
const osHostname = ref<string>('')
|
||||
|
||||
onMounted(async () => {
|
||||
osHostname.value = await getOsHostname()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column h-full">
|
||||
<div class="flex flex-column">
|
||||
<div class="w-10/12 self-center ">
|
||||
<Panel :header="t('basic_settings')">
|
||||
<div class="flex flex-column gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<div class="flex align-items-center" for="virtual_ip">
|
||||
<label class="mr-2"> {{ t('virtual_ipv4') }} </label>
|
||||
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
|
||||
|
||||
<label for="virtual_ip_auto" class="ml-2">
|
||||
{{ t('virtual_ipv4_dhcp') }}
|
||||
</label>
|
||||
</div>
|
||||
<InputGroup>
|
||||
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
||||
aria-describedby="virtual_ipv4-help" />
|
||||
<InputGroupAddon>
|
||||
<span>/</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="curNetwork.network_length" :disabled="curNetwork.dhcp"
|
||||
inputId="horizontal-buttons" showButtons :step="1" mode="decimal" :min="1" :max="32" fluid
|
||||
class="max-w-20" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="network_name">{{ t('network_name') }}</label>
|
||||
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
|
||||
</div>
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
||||
aria-describedby="network_secret-help" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="nm">{{ t('networking_method') }}</label>
|
||||
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods"
|
||||
:option-label="(v: any) => v.label()" option-value="value" />
|
||||
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
|
||||
:virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
|
||||
@complete="searchPresetPublicServers" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||
<div class="flex flex-column gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<div class="flex align-items-center">
|
||||
<Checkbox v-model="curNetwork.latency_first" input-id="use_latency_first" :binary="true" />
|
||||
<label for="use_latency_first" class="ml-2"> {{ t('use_latency_first') }} </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="hostname">{{ t('hostname') }}</label>
|
||||
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
||||
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-column gap-2 grow p-fluid">
|
||||
<label for="username">{{ t('proxy_cidrs') }}</label>
|
||||
<AutoComplete id="subnet-proxy" v-model="curNetwork.proxy_cidrs"
|
||||
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" class="w-full" multiple fluid
|
||||
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||
<div class="flex flex-column gap-2 grow">
|
||||
<label for="username">VPN Portal</label>
|
||||
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
|
||||
<div class="min-w-64">
|
||||
<InputGroup>
|
||||
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
||||
:placeholder="t('vpn_portal_client_network')" />
|
||||
<InputGroupAddon>
|
||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
|
||||
:min="0" :max="65535" class="w-8" fluid />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 grow p-fluid">
|
||||
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
||||
<AutoComplete id="listener_urls" v-model="curNetwork.listener_urls" :suggestions="listenerSuggestions"
|
||||
class="w-full" dropdown :complete-on-focus="true"
|
||||
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" multiple
|
||||
@complete="searchListenerSuggestiong" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
||||
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help"
|
||||
:format="false" :min="0" :max="65535" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="dev_name">{{ t('dev_name') }}</label>
|
||||
<InputText id="dev_name" v-model="curNetwork.dev_name" aria-describedby="dev_name-help" :format="true"
|
||||
:placeholder="t('dev_name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div class="flex pt-4 justify-content-center">
|
||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||
@click="$emit('runNetwork', curNetwork)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { EventType } from '~/types/network'
|
||||
|
||||
const props = defineProps<{
|
||||
event: {
|
||||
[key: string]: any
|
||||
}
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const eventKey = computed(() => {
|
||||
const key = Object.keys(props.event)[0]
|
||||
return Object.keys(EventType).includes(key) ? key : 'Unknown'
|
||||
})
|
||||
|
||||
const eventValue = computed(() => {
|
||||
const value = props.event[eventKey.value]
|
||||
return typeof value === 'object' ? value : value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fieldset :legend="t(`event.${eventKey}`)">
|
||||
<template v-if="eventKey !== 'Unknown'">
|
||||
<div v-if="event.DhcpIpv4Changed">
|
||||
{{ `${eventValue[0]} -> ${eventValue[1]}` }}
|
||||
</div>
|
||||
<pre v-else>{{ eventValue }}</pre>
|
||||
</template>
|
||||
<pre v-else>{{ eventValue }}</pre>
|
||||
</Fieldset>
|
||||
</template>
|
||||
@@ -1,459 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeAgo } from '@vueuse/core'
|
||||
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||
import type { NodeInfo, PeerRoutePair } from '~/types/network'
|
||||
|
||||
const props = defineProps<{
|
||||
instanceId?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
const curNetwork = computed(() => {
|
||||
if (props.instanceId) {
|
||||
// console.log('instanceId', props.instanceId)
|
||||
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
|
||||
if (c !== undefined)
|
||||
return c
|
||||
}
|
||||
|
||||
return networkStore.curNetwork
|
||||
})
|
||||
|
||||
const curNetworkInst = computed(() => {
|
||||
return networkStore.networkInstances.find(n => n.instance_id === curNetwork.value.instance_id)
|
||||
})
|
||||
|
||||
const peerRouteInfos = computed(() => {
|
||||
if (curNetworkInst.value) {
|
||||
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
||||
return [{
|
||||
route: {
|
||||
ipv4_addr: my_node_info?.virtual_ipv4,
|
||||
hostname: my_node_info?.hostname,
|
||||
version: my_node_info?.version,
|
||||
},
|
||||
}, ...(curNetworkInst.value.detail?.peer_route_pairs || [])]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
function routeCost(info: any) {
|
||||
if (info.route) {
|
||||
const cost = info.route.cost
|
||||
return cost ? cost === 1 ? 'p2p' : `relay(${cost})` : t('status.local')
|
||||
}
|
||||
|
||||
return '?'
|
||||
}
|
||||
|
||||
function resolveObjPath(path: string, obj = globalThis, separator = '.') {
|
||||
const properties = Array.isArray(path) ? path : path.split(separator)
|
||||
return properties.reduce((prev, curr) => prev?.[curr], obj)
|
||||
}
|
||||
|
||||
function statsCommon(info: any, field: string): number | undefined {
|
||||
if (!info.peer)
|
||||
return undefined
|
||||
|
||||
const conns = info.peer.conns
|
||||
return conns.reduce((acc: number, conn: any) => {
|
||||
return acc + resolveObjPath(field, conn)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function humanFileSize(bytes: number, si = false, dp = 1) {
|
||||
const thresh = si ? 1000 : 1024
|
||||
|
||||
if (Math.abs(bytes) < thresh)
|
||||
return `${bytes} B`
|
||||
|
||||
const units = si
|
||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
let u = -1
|
||||
const r = 10 ** dp
|
||||
|
||||
do {
|
||||
bytes /= thresh
|
||||
++u
|
||||
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1)
|
||||
|
||||
return `${bytes.toFixed(dp)} ${units[u]}`
|
||||
}
|
||||
|
||||
function latencyMs(info: PeerRoutePair) {
|
||||
let lat_us_sum = statsCommon(info, 'stats.latency_us')
|
||||
if (lat_us_sum === undefined)
|
||||
return ''
|
||||
lat_us_sum = lat_us_sum / 1000 / info.peer!.conns.length
|
||||
return `${lat_us_sum % 1 > 0 ? Math.round(lat_us_sum) + 1 : Math.round(lat_us_sum)}ms`
|
||||
}
|
||||
|
||||
function txBytes(info: PeerRoutePair) {
|
||||
const tx = statsCommon(info, 'stats.tx_bytes')
|
||||
return tx ? humanFileSize(tx) : ''
|
||||
}
|
||||
|
||||
function rxBytes(info: PeerRoutePair) {
|
||||
const rx = statsCommon(info, 'stats.rx_bytes')
|
||||
return rx ? humanFileSize(rx) : ''
|
||||
}
|
||||
|
||||
function lossRate(info: PeerRoutePair) {
|
||||
const lossRate = statsCommon(info, 'loss_rate')
|
||||
return lossRate !== undefined ? `${Math.round(lossRate * 100)}%` : ''
|
||||
}
|
||||
|
||||
function version(info: PeerRoutePair) {
|
||||
return info.route.version === '' ? 'unknown' : info.route.version
|
||||
}
|
||||
|
||||
function ipFormat(info: PeerRoutePair) {
|
||||
const ip = info.route.ipv4_addr
|
||||
if (typeof ip === 'string')
|
||||
return ip
|
||||
return ip ? `${num2ipv4(ip.address)}/${ip.network_length}` : ''
|
||||
}
|
||||
|
||||
const myNodeInfo = computed(() => {
|
||||
if (!curNetworkInst.value)
|
||||
return {} as NodeInfo
|
||||
|
||||
return curNetworkInst.value.detail?.my_node_info
|
||||
})
|
||||
|
||||
interface Chip {
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const myNodeInfoChips = computed(() => {
|
||||
if (!curNetworkInst.value)
|
||||
return []
|
||||
|
||||
const chips: Array<Chip> = []
|
||||
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
||||
if (!my_node_info)
|
||||
return chips
|
||||
|
||||
// TUN Device Name
|
||||
const dev_name = curNetworkInst.value.detail?.dev_name
|
||||
if (dev_name) {
|
||||
chips.push({
|
||||
label: `TUN Device Name: ${dev_name}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// virtual ipv4
|
||||
chips.push({
|
||||
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
|
||||
// local ipv4s
|
||||
const local_ipv4s = my_node_info.ips?.interface_ipv4s
|
||||
for (const [idx, ip] of local_ipv4s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv4 ${idx}: ${num2ipv4(ip)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// local ipv6s
|
||||
const local_ipv6s = my_node_info.ips?.interface_ipv6s
|
||||
for (const [idx, ip] of local_ipv6s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv6 ${idx}: ${num2ipv6(ip)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// public ip
|
||||
const public_ip = my_node_info.ips?.public_ipv4
|
||||
if (public_ip) {
|
||||
chips.push({
|
||||
label: `Public IP: ${IPv4.fromNumber(public_ip.addr)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
const public_ipv6 = my_node_info.ips?.public_ipv6
|
||||
if (public_ipv6) {
|
||||
chips.push({
|
||||
label: `Public IPv6: ${IPv6.fromBigInt((BigInt(public_ipv6.part1) << BigInt(96))
|
||||
+ (BigInt(public_ipv6.part2) << BigInt(64))
|
||||
+ (BigInt(public_ipv6.part3) << BigInt(32))
|
||||
+ BigInt(public_ipv6.part4),
|
||||
)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// listeners:
|
||||
const listeners = my_node_info.listeners
|
||||
for (const [idx, listener] of listeners?.entries()) {
|
||||
chips.push({
|
||||
label: `Listener ${idx}: ${listener}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// udp nat type
|
||||
enum NatType {
|
||||
// has NAT; but own a single public IP, port is not changed
|
||||
Unknown = 0,
|
||||
OpenInternet = 1,
|
||||
NoPAT = 2,
|
||||
FullCone = 3,
|
||||
Restricted = 4,
|
||||
PortRestricted = 5,
|
||||
Symmetric = 6,
|
||||
SymUdpFirewall = 7,
|
||||
SymmetricEasyInc = 8,
|
||||
SymmetricEasyDec = 9,
|
||||
};
|
||||
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
||||
if (udpNatType !== undefined) {
|
||||
const udpNatTypeStrMap = {
|
||||
[NatType.Unknown]: 'Unknown',
|
||||
[NatType.OpenInternet]: 'Open Internet',
|
||||
[NatType.NoPAT]: 'No PAT',
|
||||
[NatType.FullCone]: 'Full Cone',
|
||||
[NatType.Restricted]: 'Restricted',
|
||||
[NatType.PortRestricted]: 'Port Restricted',
|
||||
[NatType.Symmetric]: 'Symmetric',
|
||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||
}
|
||||
|
||||
chips.push({
|
||||
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
return chips
|
||||
})
|
||||
|
||||
function globalSumCommon(field: string) {
|
||||
let sum = 0
|
||||
if (!peerRouteInfos.value)
|
||||
return sum
|
||||
|
||||
for (const info of peerRouteInfos.value) {
|
||||
const tx = statsCommon(info, field)
|
||||
if (tx)
|
||||
sum += tx
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
function txGlobalSum() {
|
||||
return globalSumCommon('stats.tx_bytes')
|
||||
}
|
||||
|
||||
function rxGlobalSum() {
|
||||
return globalSumCommon('stats.rx_bytes')
|
||||
}
|
||||
|
||||
const peerCount = computed(() => {
|
||||
if (!peerRouteInfos.value)
|
||||
return 0
|
||||
|
||||
return peerRouteInfos.value.length
|
||||
})
|
||||
|
||||
// calculate tx/rx rate every 2 seconds
|
||||
let rateIntervalId = 0
|
||||
const rateInterval = 2000
|
||||
let prevTxSum = 0
|
||||
let prevRxSum = 0
|
||||
const txRate = ref('0')
|
||||
const rxRate = ref('0')
|
||||
onMounted(() => {
|
||||
rateIntervalId = window.setInterval(() => {
|
||||
const curTxSum = txGlobalSum()
|
||||
txRate.value = humanFileSize((curTxSum - prevTxSum) / (rateInterval / 1000))
|
||||
prevTxSum = curTxSum
|
||||
|
||||
const curRxSum = rxGlobalSum()
|
||||
rxRate.value = humanFileSize((curRxSum - prevRxSum) / (rateInterval / 1000))
|
||||
prevRxSum = curRxSum
|
||||
}, rateInterval)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(rateIntervalId)
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogContent = ref<any>('')
|
||||
const dialogHeader = ref('event_log')
|
||||
|
||||
function showVpnPortalConfig() {
|
||||
const my_node_info = myNodeInfo.value
|
||||
if (!my_node_info)
|
||||
return
|
||||
|
||||
const url = 'https://www.wireguardconfig.com/qrcode'
|
||||
dialogContent.value = `${my_node_info.vpn_portal_cfg}\n\n # can generate QR code: ${url}`
|
||||
dialogHeader.value = 'vpn_portal_config'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function showEventLogs() {
|
||||
const detail = curNetworkInst.value?.detail
|
||||
if (!detail)
|
||||
return
|
||||
|
||||
dialogContent.value = detail.events
|
||||
dialogHeader.value = 'event_log'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto">
|
||||
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
|
||||
<pre>{{ dialogContent }}</pre>
|
||||
</ScrollPanel>
|
||||
<Timeline v-else :value="dialogContent">
|
||||
<template #opposite="slotProps">
|
||||
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item[0])) }}</small>
|
||||
</template>
|
||||
<template #content="slotProps">
|
||||
<HumanEvent :event="slotProps.item[1]" />
|
||||
</template>
|
||||
</Timeline>
|
||||
</Dialog>
|
||||
|
||||
<Card v-if="curNetworkInst?.error_msg">
|
||||
<template #title>
|
||||
Run Network Error
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-column gap-y-5">
|
||||
<div class="text-red-500">
|
||||
{{ curNetworkInst.error_msg }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<template v-else>
|
||||
<Card>
|
||||
<template #title>
|
||||
{{ t('my_node_info') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex w-full flex-column gap-y-5">
|
||||
<div class="m-0 flex flex-row justify-center gap-x-5">
|
||||
<div
|
||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||
style="border: 1px solid green"
|
||||
>
|
||||
<div class="font-bold">
|
||||
{{ t('peer_count') }}
|
||||
</div>
|
||||
<div class="text-5xl mt-1">
|
||||
{{ peerCount }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||
style="border: 1px solid purple"
|
||||
>
|
||||
<div class="font-bold">
|
||||
{{ t('upload') }}
|
||||
</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ txRate }}/s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
|
||||
style="border: 1px solid fuchsia"
|
||||
>
|
||||
<div class="font-bold">
|
||||
{{ t('download') }}
|
||||
</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ rxRate }}/s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row align-items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||
<Chip
|
||||
v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
||||
class="mr-2 mt-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
|
||||
<Button severity="info" :label="t('show_vpn_portal_config')" @click="showVpnPortalConfig" />
|
||||
<Button severity="info" :label="t('show_event_log')" @click="showEventLogs" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Card>
|
||||
<template #title>
|
||||
{{ t('peer_info') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
||||
<Column :field="ipFormat" :header="t('virtual_ipv4')" />
|
||||
<Column :header="t('hostname')">
|
||||
<template #body="slotProps">
|
||||
<div
|
||||
v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
|
||||
v-tooltip="slotProps.data.route.hostname"
|
||||
>
|
||||
{{
|
||||
slotProps.data.route.hostname }}
|
||||
</div>
|
||||
<div v-else v-tooltip="slotProps.data.route.hostname" class="space-x-1">
|
||||
<Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info">
|
||||
{{ t('status.server') }}
|
||||
</Tag>
|
||||
<Tag v-if="slotProps.data.route.no_relay_data" severity="warn" value="Warn">
|
||||
{{ t('status.relay') }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column :field="routeCost" :header="t('route_cost')" />
|
||||
<Column :field="latencyMs" :header="t('latency')" />
|
||||
<Column :field="txBytes" :header="t('upload_bytes')" />
|
||||
<Column :field="rxBytes" :header="t('download_bytes')" />
|
||||
<Column :field="lossRate" :header="t('loss_rate')" />
|
||||
<Column :header="t('status.version')">
|
||||
<template #body="slotProps">
|
||||
<span>{{ version(slotProps.data) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.p-timeline :deep(.p-timeline-event-opposite) {
|
||||
@apply flex-none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user