mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 02:09:06 +00:00
introduce gui based on tauri (#52)
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
|
||||
import Stepper from 'primevue/stepper';
|
||||
import StepperPanel from 'primevue/stepperpanel';
|
||||
|
||||
import { useToast } from "primevue/usetoast";
|
||||
|
||||
import { i18n, loadLocaleFromLocalStorage, NetworkConfig, parseNetworkConfig,
|
||||
useNetworkStore, runNetworkInstance, retainNetworkInstance, collectNetworkInfos,
|
||||
changeLocale } from './main';
|
||||
|
||||
import Config from './components/Config.vue';
|
||||
import Status from './components/Status.vue';
|
||||
|
||||
import { exit } from '@tauri-apps/api/process';
|
||||
|
||||
const visible = ref(false);
|
||||
const tomlConfig = ref("");
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: () => i18n.global.t('show_config'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: async () => {
|
||||
try {
|
||||
const ret = await parseNetworkConfig(networkStore.curNetwork);
|
||||
tomlConfig.value = ret;
|
||||
} catch (e: any) {
|
||||
tomlConfig.value = e;
|
||||
}
|
||||
visible.value = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: () => i18n.global.t('del_cur_network'),
|
||||
icon: 'pi pi-times',
|
||||
command: () => {
|
||||
networkStore.delCurNetwork();
|
||||
},
|
||||
disabled: () => networkStore.networkList.length <= 1,
|
||||
},
|
||||
])
|
||||
|
||||
enum Severity {
|
||||
None = "none",
|
||||
Success = "success",
|
||||
Info = "info",
|
||||
Warn = "warn",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
const messageBarSeverity = ref(Severity.None);
|
||||
const messageBarContent = ref("");
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const networkStore = useNetworkStore();
|
||||
|
||||
const addNewNetwork = () => {
|
||||
networkStore.addNewNetwork();
|
||||
networkStore.curNetwork = networkStore.lastNetwork;
|
||||
}
|
||||
|
||||
const networkMenuName = (network: NetworkConfig) => {
|
||||
return network.network_name + " (" + network.instance_id + ")";
|
||||
}
|
||||
|
||||
networkStore.$subscribe(async () => {
|
||||
networkStore.saveToLocalStroage();
|
||||
try {
|
||||
await parseNetworkConfig(networkStore.curNetwork);
|
||||
messageBarSeverity.value = Severity.None;
|
||||
} catch (e: any) {
|
||||
messageBarContent.value = e;
|
||||
messageBarSeverity.value = Severity.Error;
|
||||
}
|
||||
});
|
||||
|
||||
async function runNetworkCb(cfg: NetworkConfig, cb: (e: MouseEvent) => void) {
|
||||
cb({} as MouseEvent);
|
||||
networkStore.removeNetworkInstance(cfg.instance_id);
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds);
|
||||
networkStore.addNetworkInstance(cfg.instance_id);
|
||||
|
||||
try {
|
||||
await runNetworkInstance(cfg);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'info', detail: e });
|
||||
}
|
||||
}
|
||||
|
||||
async function stopNetworkCb(cfg: NetworkConfig, cb: (e: MouseEvent) => void) {
|
||||
console.log("stopNetworkCb", cfg, cb);
|
||||
cb({} as MouseEvent);
|
||||
networkStore.removeNetworkInstance(cfg.instance_id);
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds);
|
||||
}
|
||||
|
||||
async function updateNetworkInfos() {
|
||||
networkStore.updateWithNetworkInfos(await collectNetworkInfos());
|
||||
}
|
||||
|
||||
let intervalId = 0;
|
||||
onMounted(() => {
|
||||
intervalId = setInterval(async () => {
|
||||
await updateNetworkInfos();
|
||||
}, 500);
|
||||
});
|
||||
onUnmounted(() => clearInterval(intervalId))
|
||||
|
||||
const curNetworkHasInstance = computed(() => {
|
||||
return networkStore.networkInstanceIds.includes(networkStore.curNetworkId);
|
||||
});
|
||||
|
||||
const activeStep = computed(() => {
|
||||
return curNetworkHasInstance.value ? 1 : 0;
|
||||
});
|
||||
|
||||
const setting_menu = ref();
|
||||
const setting_menu_items = ref([
|
||||
{
|
||||
label: () => i18n.global.t('settings'),
|
||||
items: [
|
||||
{
|
||||
label: () => i18n.global.t('exchange_language'),
|
||||
icon: 'pi pi-refresh',
|
||||
command: () => {
|
||||
changeLocale((i18n.global.locale.value === 'en' ? 'cn' : 'en'));
|
||||
}
|
||||
},
|
||||
{
|
||||
label: () => i18n.global.t('exit'),
|
||||
icon: 'pi pi-times',
|
||||
command: async () => {
|
||||
await exit(1);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
const toggle_setting_menu = (event: any) => {
|
||||
setting_menu.value.toggle(event);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
networkStore.loadFromLocalStorage();
|
||||
loadLocaleFromLocalStorage();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <n-config-provider :theme="lightTheme"> -->
|
||||
<div id="root" class="flex flex-column">
|
||||
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
|
||||
<Panel>
|
||||
<ScrollPanel style="width: 100%; height: 300px">
|
||||
<pre>{{ tomlConfig }}</pre>
|
||||
</ScrollPanel>
|
||||
</Panel>
|
||||
<Divider />
|
||||
<div class="flex justify-content-end gap-2">
|
||||
<Button type="button" :label="$t('close')" @click="visible = false"></Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<div>
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<div class="flex align-items-center gap-2">
|
||||
<Button icon="pi pi-plus" class="mr-2" severity="primary" :label="$t('add_new_network')"
|
||||
@click="addNewNetwork" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #center>
|
||||
<div class="min-w-80 mr-20">
|
||||
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList"
|
||||
:optionLabel="networkMenuName" :placeholder="$t('select_network')" :highlightOnSelect="true"
|
||||
:checkmark="true" class="w-full md:w-32rem" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<Button icon="pi pi-cog" class="mr-2" severity="secondary" aria-haspopup="true" @click="toggle_setting_menu"
|
||||
:label="$t('settings')" aria-controls="overlay_setting_menu" />
|
||||
<Menu ref="setting_menu" id="overlay_setting_menu" :model="setting_menu_items" :popup="true" />
|
||||
</template>
|
||||
</Toolbar>
|
||||
</div>
|
||||
|
||||
<Stepper class="h-full overflow-y-auto" :active-step="activeStep">
|
||||
<StepperPanel :header="$t('config_network')" class="w">
|
||||
<template #content="{ nextCallback }">
|
||||
<Config @run-network="runNetworkCb($event, nextCallback)" :instance-id="networkStore.curNetworkId"
|
||||
:config-invalid="messageBarSeverity != Severity.None" />
|
||||
</template>
|
||||
</StepperPanel>
|
||||
<StepperPanel :header="$t('running')">
|
||||
<template #content="{ prevCallback }">
|
||||
<div class="flex flex-column">
|
||||
<Status :instance-id="networkStore.curNetworkId" />
|
||||
</div>
|
||||
<div class="flex pt-4 justify-content-center">
|
||||
<Button label="Stop Network" severity="danger" icon="pi pi-arrow-left"
|
||||
@click="stopNetworkCb(networkStore.curNetwork, prevCallback)" />
|
||||
</div>
|
||||
</template>
|
||||
</StepperPanel>
|
||||
</Stepper>
|
||||
|
||||
<div>
|
||||
<Menubar :model="items" breakpoint="300px">
|
||||
</Menubar>
|
||||
<InlineMessage v-if="messageBarSeverity !== Severity.None" class="absolute bottom-0 right-0" severity="error">
|
||||
{{ messageBarContent }}</InlineMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#root {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.p-menubar .p-menuitem {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
.p-tabview-panel {
|
||||
height: 100%;
|
||||
} */
|
||||
</style>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
</script>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import InputGroup from "primevue/inputgroup";
|
||||
import InputGroupAddon from "primevue/inputgroupaddon";
|
||||
import { ref, defineProps, computed } from "vue";
|
||||
import { i18n, useNetworkStore, NetworkingMethod } from "../main";
|
||||
|
||||
|
||||
const networking_methods = ref([
|
||||
{ value: NetworkingMethod.PublicServer, label: i18n.global.t('public_server') },
|
||||
{ value: NetworkingMethod.Manual, label: i18n.global.t('manual') },
|
||||
{ value: NetworkingMethod.Standalone, label: i18n.global.t('standalone') },
|
||||
]);
|
||||
|
||||
const props = defineProps<{
|
||||
configInvalid?: boolean,
|
||||
instanceId?: string,
|
||||
}>()
|
||||
|
||||
defineEmits(["runNetwork"]);
|
||||
|
||||
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 presetPublicServers = [
|
||||
"tcp://easytier.public.kkrainbow.top:11010",
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column h-full">
|
||||
<div class="flex flex-column">
|
||||
<div class="w-10/12 max-w-fit self-center ">
|
||||
<Panel header="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">
|
||||
<label for="virtual_ip">{{ $t('virtual_ipv4') }}</label>
|
||||
<InputGroup>
|
||||
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" aria-describedby="virtual_ipv4-help" />
|
||||
<InputGroupAddon>
|
||||
<span>/24</span>
|
||||
</InputGroupAddon>
|
||||
</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>
|
||||
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||
<Dropdown v-model="curNetwork.networking_method" :options="networking_methods" optionLabel="label"
|
||||
optionValue="value" placeholder="Select Method" class="" />
|
||||
<Chips id="chips" v-model="curNetwork.peer_urls"
|
||||
:placeholder="$t('chips_placeholder', ['tcp://8.8.8.8:11010'])" separator=" " class="grow"
|
||||
v-if="curNetwork.networking_method == NetworkingMethod.Manual" />
|
||||
|
||||
<Dropdown :editable="true" v-model="curNetwork.public_server_url" class="grow"
|
||||
:options="presetPublicServers"
|
||||
v-if="curNetwork.networking_method == NetworkingMethod.PublicServer" />
|
||||
</div>
|
||||
</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>
|
||||
<Chips id="chips" v-model="curNetwork.proxy_cidrs"
|
||||
:placeholder="$t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" />
|
||||
</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>
|
||||
<div class="items-center flex flex-row gap-x-4">
|
||||
<ToggleButton onIcon="pi pi-check" offIcon="pi pi-times" v-model="curNetwork.enable_vpn_portal"
|
||||
:onLabel="$t('off_text')" :offLabel="$t('on_text')" />
|
||||
<div class="grow" v-if="curNetwork.enable_vpn_portal">
|
||||
<InputGroup>
|
||||
<InputText :placeholder="$t('vpn_portal_client_network')"
|
||||
v-model="curNetwork.vpn_portal_client_network_addr" />
|
||||
<InputGroupAddon>
|
||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<InputNumber :placeholder="$t('vpn_portal_listen_port')" class="" v-if="curNetwork.enable_vpn_portal"
|
||||
:format="false" v-model="curNetwork.vpn_portal_listne_port" :min="0" :max="65535" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel :header="$t('advanced_settings')" toggleable>
|
||||
<div class="flex flex-column gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-column gap-2 grow p-fluid">
|
||||
<label for="listener_urls">{{ $t('listener_urls') }}</label>
|
||||
<Chips id="listener_urls" v-model="curNetwork.listener_urls"
|
||||
:placeholder="$t('chips_placeholder', ['tcp://1.1.1.1:11010'])" separator=" " class="w-full" />
|
||||
</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="username-help"
|
||||
:format="false" :min="0" :max="65535" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
|
||||
<div class="flex pt-4 justify-content-center">
|
||||
<Button label="Run Network" icon="pi pi-arrow-right" iconPos="right" @click="$emit('runNetwork', curNetwork)"
|
||||
:disabled="configInvalid" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,358 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useNetworkStore } from '../main';
|
||||
|
||||
const networkStore = useNetworkStore();
|
||||
|
||||
const props = defineProps<{
|
||||
instanceId?: string,
|
||||
}>()
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
let curNetworkInst = computed(() => {
|
||||
return networkStore.networkInstances.find(n => n.instance_id == curNetwork.value.instance_id);
|
||||
});
|
||||
|
||||
let peerRouteInfos = computed(() => {
|
||||
if (curNetworkInst.value) {
|
||||
return curNetworkInst.value.detail.peer_route_pairs;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
let routeCost = (info: any) => {
|
||||
if (info.route) {
|
||||
const cost = info.route.cost;
|
||||
return cost == 1 ? "p2p" : `relay(${cost})`
|
||||
}
|
||||
return '?';
|
||||
};
|
||||
|
||||
function resolveObjPath(path: string, obj = self, separator = '.') {
|
||||
var properties = Array.isArray(path) ? path : path.split(separator)
|
||||
return properties.reduce((prev, curr) => prev?.[curr], obj)
|
||||
}
|
||||
|
||||
let statsCommon = (info: any, field: string) => {
|
||||
if (!info.peer) {
|
||||
return undefined;
|
||||
}
|
||||
let 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];
|
||||
}
|
||||
|
||||
let latencyMs = (info: any) => {
|
||||
let lat_us_sum = statsCommon(info, 'stats.latency_us');
|
||||
return lat_us_sum ? `${lat_us_sum / 1000 / info.peer.conns.length}ms` : '';
|
||||
};
|
||||
|
||||
let txBytes = (info: any) => {
|
||||
let tx = statsCommon(info, 'stats.tx_bytes');
|
||||
return tx ? humanFileSize(tx) : '';
|
||||
}
|
||||
|
||||
let rxBytes = (info: any) => {
|
||||
let rx = statsCommon(info, 'stats.rx_bytes');
|
||||
return rx ? humanFileSize(rx) : '';
|
||||
}
|
||||
|
||||
let lossRate = (info: any) => {
|
||||
let lossRate = statsCommon(info, 'loss_rate');
|
||||
return lossRate != undefined ? `${Math.round(lossRate * 100)}%` : '';
|
||||
}
|
||||
|
||||
const myNodeInfo = computed(() => {
|
||||
if (!curNetworkInst.value) {
|
||||
return {};
|
||||
}
|
||||
return curNetworkInst.value.detail?.my_node_info;
|
||||
});
|
||||
|
||||
interface Chip {
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
let myNodeInfoChips = computed(() => {
|
||||
if (!curNetworkInst.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let chips: Array<Chip> = [];
|
||||
let my_node_info = curNetworkInst.value.detail?.my_node_info;
|
||||
if (!my_node_info) {
|
||||
return chips;
|
||||
}
|
||||
|
||||
// local ipv4s
|
||||
let local_ipv4s = my_node_info.ips?.interface_ipv4s;
|
||||
for (let [idx, ip] of local_ipv4s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv4 ${idx}: ${ip}`,
|
||||
icon: '',
|
||||
} as Chip);
|
||||
}
|
||||
|
||||
// local ipv6s
|
||||
let local_ipv6s = my_node_info.ips?.interface_ipv6s;
|
||||
for (let [idx, ip] of local_ipv6s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv6 ${idx}: ${ip}`,
|
||||
icon: '',
|
||||
} as Chip);
|
||||
}
|
||||
|
||||
// public ip
|
||||
let public_ip = my_node_info.ips?.public_ipv4;
|
||||
if (public_ip) {
|
||||
chips.push({
|
||||
label: `Public IP: ${public_ip}`,
|
||||
icon: '',
|
||||
} as Chip);
|
||||
}
|
||||
|
||||
|
||||
// listeners:
|
||||
let listeners = my_node_info.listeners;
|
||||
for (let [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,
|
||||
};
|
||||
let udpNatType: NatType = my_node_info.stun_info?.udp_nat_type;
|
||||
if (udpNatType != undefined) {
|
||||
let 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',
|
||||
};
|
||||
|
||||
chips.push({
|
||||
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
|
||||
icon: '',
|
||||
} as Chip);
|
||||
|
||||
}
|
||||
|
||||
return chips;
|
||||
});
|
||||
|
||||
const globalSumCommon = (field: string) => {
|
||||
let sum = 0;
|
||||
if (!peerRouteInfos.value) {
|
||||
return sum;
|
||||
}
|
||||
for (let info of peerRouteInfos.value) {
|
||||
let tx = statsCommon(info, field);
|
||||
if (tx) {
|
||||
sum += tx;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
};
|
||||
|
||||
const txGlobalSum = () => {
|
||||
return globalSumCommon('stats.tx_bytes');
|
||||
};
|
||||
|
||||
const 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;
|
||||
let rateInterval = 2000;
|
||||
let prevTxSum = 0;
|
||||
let prevRxSum = 0;
|
||||
let txRate = ref('0');
|
||||
let rxRate = ref('0');
|
||||
onMounted(() => {
|
||||
rateIntervalId = setInterval(() => {
|
||||
let curTxSum = txGlobalSum();
|
||||
txRate.value = humanFileSize((curTxSum - prevTxSum) / (rateInterval / 1000));
|
||||
prevTxSum = curTxSum;
|
||||
|
||||
let curRxSum = rxGlobalSum();
|
||||
rxRate.value = humanFileSize((curRxSum - prevRxSum) / (rateInterval / 1000));
|
||||
prevRxSum = curRxSum;
|
||||
}, rateInterval);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(rateIntervalId);
|
||||
});
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogContent = ref('');
|
||||
|
||||
const showVpnPortalConfig = () => {
|
||||
let 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}`;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
const showEventLogs = () => {
|
||||
let detail = curNetworkInst.value?.detail;
|
||||
if (!detail) {
|
||||
return;
|
||||
}
|
||||
dialogContent.value = detail.events;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dialog v-model:visible="dialogVisible" modal header="Dialog" :style="{ width: '70%' }">
|
||||
<Panel>
|
||||
<ScrollPanel style="width: 100%; height: 400px">
|
||||
<pre>{{ dialogContent }}</pre>
|
||||
</ScrollPanel>
|
||||
</Panel>
|
||||
<Divider />
|
||||
<div class="flex justify-content-end gap-2">
|
||||
<Button type="button" label="Close" @click="dialogVisible = false"></Button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<Card v-if="!curNetworkInst?.error_msg">
|
||||
<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-36 h-36 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-36 h-36 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-36 h-36 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">
|
||||
<Chip v-for="chip in myNodeInfoChips" :label="chip.label" :icon="chip.icon" class="mr-2 mt-2">
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
<div class="m-0 flex flex-row justify-center gap-x-5 text-sm" v-if="myNodeInfo">
|
||||
<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 v-if="!curNetworkInst?.error_msg">
|
||||
<template #title>{{ $t('peer_info') }}</template>
|
||||
<template #content>
|
||||
<DataTable :value="peerRouteInfos" tableStyle="min-width: 50rem">
|
||||
<Column field="route.ipv4_addr" :header="$t('virtual_ipv4')"></Column>
|
||||
<Column field="route.hostname" :header="$t('hostname')"></Column>
|
||||
<Column :field="routeCost" :header="$t('route_cost')"></Column>
|
||||
<Column :field="latencyMs" :header="$t('latency')"></Column>
|
||||
<Column :field="txBytes" :header="$t('upload_bytes')"></Column>
|
||||
<Column :field="rxBytes" :header="$t('download_bytes')"></Column>
|
||||
<Column :field="lossRate" :header="$t('loss_rate')"></Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,217 +0,0 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{atomic::AtomicBool, Arc, RwLock},
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Local};
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConfigLoader, TomlConfigLoader},
|
||||
global_ctx::GlobalCtxEvent,
|
||||
stun::StunInfoCollectorTrait,
|
||||
},
|
||||
instance::instance::Instance,
|
||||
peers::rpc_service::PeerManagerRpcService,
|
||||
rpc::{
|
||||
cli::{PeerInfo, Route, StunInfo},
|
||||
peer::GetIpListResponse,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MyNodeInfo {
|
||||
pub virtual_ipv4: String,
|
||||
pub ips: GetIpListResponse,
|
||||
pub stun_info: StunInfo,
|
||||
pub listeners: Vec<String>,
|
||||
pub vpn_portal_cfg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct EasyTierData {
|
||||
events: Arc<RwLock<VecDeque<(DateTime<Local>, GlobalCtxEvent)>>>,
|
||||
node_info: Arc<RwLock<MyNodeInfo>>,
|
||||
routes: Arc<RwLock<Vec<Route>>>,
|
||||
peers: Arc<RwLock<Vec<PeerInfo>>>,
|
||||
}
|
||||
|
||||
pub struct EasyTierLauncher {
|
||||
instance_alive: Arc<AtomicBool>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
thread_handle: Option<std::thread::JoinHandle<()>>,
|
||||
running_cfg: String,
|
||||
|
||||
error_msg: Arc<RwLock<Option<String>>>,
|
||||
data: EasyTierData,
|
||||
}
|
||||
|
||||
impl EasyTierLauncher {
|
||||
pub fn new() -> Self {
|
||||
let instance_alive = Arc::new(AtomicBool::new(false));
|
||||
Self {
|
||||
instance_alive,
|
||||
thread_handle: None,
|
||||
error_msg: Arc::new(RwLock::new(None)),
|
||||
running_cfg: String::new(),
|
||||
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
data: EasyTierData::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_easytier_event(event: GlobalCtxEvent, data: EasyTierData) {
|
||||
let mut events = data.events.write().unwrap();
|
||||
events.push_back((chrono::Local::now(), event));
|
||||
if events.len() > 100 {
|
||||
events.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
async fn easytier_routine(
|
||||
cfg: TomlConfigLoader,
|
||||
stop_signal: Arc<tokio::sync::Notify>,
|
||||
data: EasyTierData,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut instance = Instance::new(cfg);
|
||||
let peer_mgr = instance.get_peer_manager();
|
||||
|
||||
// Subscribe to global context events
|
||||
let global_ctx = instance.get_global_ctx();
|
||||
let data_c = data.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut receiver = global_ctx.subscribe();
|
||||
while let Ok(event) = receiver.recv().await {
|
||||
Self::handle_easytier_event(event, data_c.clone()).await;
|
||||
}
|
||||
});
|
||||
|
||||
// update my node info
|
||||
let data_c = data.clone();
|
||||
let global_ctx_c = instance.get_global_ctx();
|
||||
let peer_mgr_c = peer_mgr.clone();
|
||||
let vpn_portal = instance.get_vpn_portal_inst();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let node_info = MyNodeInfo {
|
||||
virtual_ipv4: global_ctx_c
|
||||
.get_ipv4()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default(),
|
||||
ips: global_ctx_c.get_ip_collector().collect_ip_addrs().await,
|
||||
stun_info: global_ctx_c.get_stun_info_collector().get_stun_info(),
|
||||
listeners: global_ctx_c
|
||||
.get_running_listeners()
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
vpn_portal_cfg: Some(
|
||||
vpn_portal
|
||||
.lock()
|
||||
.await
|
||||
.dump_client_config(peer_mgr_c.clone())
|
||||
.await,
|
||||
),
|
||||
};
|
||||
*data_c.node_info.write().unwrap() = node_info.clone();
|
||||
*data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await;
|
||||
*data_c.peers.write().unwrap() = PeerManagerRpcService::new(peer_mgr_c.clone())
|
||||
.list_peers()
|
||||
.await;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
});
|
||||
|
||||
instance.run().await?;
|
||||
stop_signal.notified().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start<F>(&mut self, cfg_generator: F)
|
||||
where
|
||||
F: FnOnce() -> Result<TomlConfigLoader, anyhow::Error> + Send + Sync,
|
||||
{
|
||||
let error_msg = self.error_msg.clone();
|
||||
let cfg = cfg_generator();
|
||||
if let Err(e) = cfg {
|
||||
error_msg.write().unwrap().replace(e.to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
self.running_cfg = cfg.as_ref().unwrap().dump();
|
||||
|
||||
let stop_flag = self.stop_flag.clone();
|
||||
|
||||
let instance_alive = self.instance_alive.clone();
|
||||
instance_alive.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let data = self.data.clone();
|
||||
|
||||
self.thread_handle = Some(std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let stop_notifier = Arc::new(tokio::sync::Notify::new());
|
||||
|
||||
let stop_notifier_clone = stop_notifier.clone();
|
||||
rt.spawn(async move {
|
||||
while !stop_flag.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
stop_notifier_clone.notify_one();
|
||||
});
|
||||
|
||||
let ret = rt.block_on(Self::easytier_routine(
|
||||
cfg.unwrap(),
|
||||
stop_notifier.clone(),
|
||||
data,
|
||||
));
|
||||
if let Err(e) = ret {
|
||||
error_msg.write().unwrap().replace(e.to_string());
|
||||
}
|
||||
instance_alive.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn error_msg(&self) -> Option<String> {
|
||||
self.error_msg.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn running(&self) -> bool {
|
||||
self.instance_alive
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn get_events(&self) -> Vec<(DateTime<Local>, GlobalCtxEvent)> {
|
||||
let events = self.data.events.read().unwrap();
|
||||
events.iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn get_node_info(&self) -> MyNodeInfo {
|
||||
self.data.node_info.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_routes(&self) -> Vec<Route> {
|
||||
self.data.routes.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_peers(&self) -> Vec<PeerInfo> {
|
||||
self.data.peers.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn running_cfg(&self) -> String {
|
||||
self.running_cfg.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EasyTierLauncher {
|
||||
fn drop(&mut self) {
|
||||
self.stop_flag
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(handle) = self.thread_handle.take() {
|
||||
if let Err(e) = handle.join() {
|
||||
println!("Error when joining thread: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,353 @@
|
||||
import "./styles.css";
|
||||
import "primevue/resources/themes/aura-light-green/theme.css";
|
||||
import "primeicons/primeicons.css";
|
||||
import "primeflex/primeflex.css";
|
||||
|
||||
import { createPinia, defineStore } from 'pinia'
|
||||
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
|
||||
import { createApp } from "vue";
|
||||
import PrimeVue from 'primevue/config';
|
||||
import App from "./App.vue";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import ToastService from 'primevue/toastservice';
|
||||
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
export enum NetworkingMethod {
|
||||
PublicServer = "PublicServer",
|
||||
Manual = "Manual",
|
||||
Standalone = "Standalone",
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
instance_id: string,
|
||||
|
||||
virtual_ipv4: string
|
||||
network_name: string
|
||||
network_secret: string
|
||||
|
||||
networking_method: NetworkingMethod,
|
||||
|
||||
public_server_url: string,
|
||||
peer_urls: Array<string>,
|
||||
|
||||
proxy_cidrs: Array<string>,
|
||||
|
||||
enable_vpn_portal: boolean,
|
||||
vpn_portal_listne_port: number,
|
||||
vpn_portal_client_network_addr: string,
|
||||
vpn_portal_client_network_len: number,
|
||||
|
||||
advanced_settings: boolean,
|
||||
|
||||
listener_urls: Array<string>,
|
||||
rpc_port: number,
|
||||
}
|
||||
|
||||
function default_network(): NetworkConfig {
|
||||
return {
|
||||
instance_id: uuidv4(),
|
||||
|
||||
virtual_ipv4: "",
|
||||
network_name: "default",
|
||||
network_secret: "",
|
||||
|
||||
networking_method: NetworkingMethod.PublicServer,
|
||||
|
||||
public_server_url: "tcp://easytier.public.kkrainbow.top:11010",
|
||||
peer_urls: [],
|
||||
|
||||
proxy_cidrs: [],
|
||||
|
||||
enable_vpn_portal: false,
|
||||
vpn_portal_listne_port: 22022,
|
||||
vpn_portal_client_network_addr: "",
|
||||
vpn_portal_client_network_len: 24,
|
||||
|
||||
advanced_settings: false,
|
||||
|
||||
listener_urls: [
|
||||
"tcp://0.0.0.0:11010",
|
||||
"udp://0.0.0.0:11010",
|
||||
"wg://0.0.0.0:11011",
|
||||
],
|
||||
rpc_port: 15888,
|
||||
}
|
||||
}
|
||||
|
||||
export interface NetworkInstance {
|
||||
instance_id: string,
|
||||
|
||||
running: boolean,
|
||||
error_msg: string,
|
||||
|
||||
detail: any,
|
||||
}
|
||||
|
||||
export const useNetworkStore = defineStore('network', {
|
||||
state: () => {
|
||||
const networkList = [default_network()];
|
||||
return {
|
||||
// for initially empty lists
|
||||
networkList: networkList as NetworkConfig[],
|
||||
// for data that is not yet loaded
|
||||
curNetwork: networkList[0],
|
||||
|
||||
// uuid -> instance
|
||||
instances: {} as Record<string, NetworkInstance>,
|
||||
|
||||
networkInfos: {} as Record<string, any>,
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
lastNetwork(): NetworkConfig {
|
||||
return this.networkList[this.networkList.length - 1];
|
||||
},
|
||||
|
||||
curNetworkId(): string {
|
||||
return this.curNetwork.instance_id;
|
||||
},
|
||||
|
||||
networkInstances(): Array<NetworkInstance> {
|
||||
return Object.values(this.instances);
|
||||
},
|
||||
|
||||
networkInstanceIds(): Array<string> {
|
||||
return Object.keys(this.instances);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
addNewNetwork() {
|
||||
this.networkList.push(default_network());
|
||||
},
|
||||
|
||||
delCurNetwork() {
|
||||
const curNetworkIdx = this.networkList.indexOf(this.curNetwork);
|
||||
this.networkList.splice(curNetworkIdx, 1);
|
||||
const nextCurNetworkIdx = Math.min(curNetworkIdx, this.networkList.length - 1);
|
||||
this.curNetwork = this.networkList[nextCurNetworkIdx];
|
||||
},
|
||||
|
||||
removeNetworkInstance(instanceId: string) {
|
||||
delete this.instances[instanceId];
|
||||
},
|
||||
|
||||
addNetworkInstance(instanceId: string) {
|
||||
this.instances[instanceId] = {
|
||||
instance_id: instanceId,
|
||||
running: false,
|
||||
error_msg: "",
|
||||
detail: {},
|
||||
};
|
||||
},
|
||||
|
||||
updateWithNetworkInfos(networkInfos: Record<string, any>) {
|
||||
this.networkInfos = networkInfos;
|
||||
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
||||
if (this.instances[instanceId] === undefined) {
|
||||
this.addNetworkInstance(instanceId);
|
||||
}
|
||||
this.instances[instanceId].running = info["running"];
|
||||
this.instances[instanceId].error_msg = info["error_msg"];
|
||||
this.instances[instanceId].detail = info;
|
||||
}
|
||||
},
|
||||
|
||||
loadFromLocalStorage() {
|
||||
const networkList = JSON.parse(localStorage.getItem("networkList") || '[]');
|
||||
let result = [];
|
||||
for (const cfg of networkList) {
|
||||
result.push({
|
||||
...default_network(),
|
||||
...cfg,
|
||||
});
|
||||
}
|
||||
if (result.length === 0) {
|
||||
result.push(default_network());
|
||||
}
|
||||
this.networkList = result;
|
||||
this.curNetwork = this.networkList[0];
|
||||
},
|
||||
|
||||
saveToLocalStroage() {
|
||||
localStorage.setItem("networkList", JSON.stringify(this.networkList));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig): Promise<string> {
|
||||
const ret: string = await invoke("parse_network_config", { cfg: JSON.stringify(cfg) });
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function runNetworkInstance(cfg: NetworkConfig) {
|
||||
const ret: string = await invoke("run_network_instance", { cfg: JSON.stringify(cfg) });
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function retainNetworkInstance(instanceIds: Array<string>) {
|
||||
const ret: string = await invoke("retain_network_instance", { instanceIds: JSON.stringify(instanceIds) });
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function collectNetworkInfos() {
|
||||
const ret: string = await invoke("collect_network_infos", {});
|
||||
return JSON.parse(ret);
|
||||
}
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
"network": "Network",
|
||||
"networking_method": "Networking Method",
|
||||
"public_server": "Public Server",
|
||||
"manual": "Manual",
|
||||
"standalone": "Standalone",
|
||||
"virtual_ipv4": "Virtual IPv4",
|
||||
"network_name": "Network Name",
|
||||
"network_secret": "Network Secret",
|
||||
"public_server_url": "Public Server URL",
|
||||
"peer_urls": "Peer URLs",
|
||||
"proxy_cidrs": "Subnet Proxy CIDRs",
|
||||
"enable_vpn_portal": "Enable VPN Portal",
|
||||
"vpn_portal_listen_port": "VPN Portal Listen Port",
|
||||
"vpn_portal_client_network": "Client Sub Network",
|
||||
"advanced_settings": "Advanced Settings",
|
||||
"listener_urls": "Listener URLs",
|
||||
"rpc_port": "RPC Port",
|
||||
"config_network": "Config Network",
|
||||
"running": "Running",
|
||||
"error_msg": "Error Message",
|
||||
"detail": "Detail",
|
||||
"add_new_network": "Add New Network",
|
||||
"del_cur_network": "Delete Current Network",
|
||||
"select_network": "Select Network",
|
||||
"network_instances": "Network Instances",
|
||||
"instance_id": "Instance ID",
|
||||
"network_infos": "Network Infos",
|
||||
"parse_network_config": "Parse Network Config",
|
||||
"run_network_instance": "Run Network Instance",
|
||||
"retain_network_instance": "Retain Network Instance",
|
||||
"collect_network_infos": "Collect Network Infos",
|
||||
"settings": "Settings",
|
||||
"exchange_language": "切换中文",
|
||||
"exit": "Exit",
|
||||
|
||||
"chips_placeholder": "e.g: {0}, press Enter to add",
|
||||
"off_text": "Press to disable",
|
||||
"on_text": "Press to enable",
|
||||
|
||||
"show_config": "Show Config",
|
||||
"close": "Close",
|
||||
|
||||
"my_node_info": "My Node Info",
|
||||
"peer_count": "Connected",
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"show_vpn_portal_config": "Show VPN Portal Config",
|
||||
"show_event_log": "Show Event Log",
|
||||
"peer_info": "Peer Info",
|
||||
"route_cost": "Route Cost",
|
||||
"hostname": "Hostname",
|
||||
"latency": "Latency",
|
||||
"upload_bytes": "Upload",
|
||||
"download_bytes": "Download",
|
||||
"loss_rate": "Loss Rate",
|
||||
},
|
||||
cn: {
|
||||
"network": "网络",
|
||||
"networking_method": "网络方式",
|
||||
"public_server": "公共服务器",
|
||||
"manual": "手动",
|
||||
"standalone": "独立",
|
||||
"virtual_ipv4": "虚拟IPv4地址",
|
||||
"network_name": "网络名称",
|
||||
"network_secret": "网络密码",
|
||||
"public_server_url": "公共服务器地址",
|
||||
"peer_urls": "对等节点地址",
|
||||
"proxy_cidrs": "子网代理CIDR",
|
||||
"enable_vpn_portal": "启用VPN门户",
|
||||
"vpn_portal_listen_port": "监听端口",
|
||||
"vpn_portal_client_network": "客户端子网",
|
||||
"advanced_settings": "高级设置",
|
||||
"listener_urls": "监听地址",
|
||||
"rpc_port": "RPC端口",
|
||||
"config_network": "配置网络",
|
||||
"running": "运行中",
|
||||
"error_msg": "错误信息",
|
||||
"detail": "详情",
|
||||
"add_new_network": "添加新网络",
|
||||
"del_cur_network": "删除当前网络",
|
||||
"select_network": "选择网络",
|
||||
"network_instances": "网络实例",
|
||||
"instance_id": "实例ID",
|
||||
"network_infos": "网络信息",
|
||||
"parse_network_config": "解析网络配置",
|
||||
"run_network_instance": "运行网络实例",
|
||||
"retain_network_instance": "保留网络实例",
|
||||
"collect_network_infos": "收集网络信息",
|
||||
"settings": "设置",
|
||||
"exchange_language": "Switch to English",
|
||||
"exit": "退出",
|
||||
"chips_placeholder": "例如: {0}, 按回车添加",
|
||||
"off_text": "点击关闭",
|
||||
"on_text": "点击开启",
|
||||
"show_config": "显示配置",
|
||||
"close": "关闭",
|
||||
|
||||
"my_node_info": "当前节点信息",
|
||||
"peer_count": "已连接",
|
||||
"upload": "上传",
|
||||
"download": "下载",
|
||||
"show_vpn_portal_config": "显示VPN门户配置",
|
||||
"show_event_log": "显示事件日志",
|
||||
"peer_info": "节点信息",
|
||||
"hostname": "主机名",
|
||||
"route_cost": "路由",
|
||||
"latency": "延迟",
|
||||
"upload_bytes": "上传",
|
||||
"download_bytes": "下载",
|
||||
"loss_rate": "丢包率",
|
||||
}
|
||||
}
|
||||
|
||||
function saveLocaleToLocalStorage(locale: string) {
|
||||
localStorage.setItem("locale", locale);
|
||||
}
|
||||
|
||||
export function loadLocaleFromLocalStorage(): string {
|
||||
return localStorage.getItem("locale") || "en";
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en', // set locale
|
||||
fallbackLocale: 'cn', // set fallback locale
|
||||
messages,
|
||||
})
|
||||
|
||||
export function changeLocale(locale: 'en' | 'cn') {
|
||||
i18n.global.locale.value = locale;
|
||||
saveLocaleToLocalStorage(locale);
|
||||
}
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(i18n, { useScope: 'global' })
|
||||
app.use(pinia)
|
||||
app.use(PrimeVue);
|
||||
app.use(ToastService);
|
||||
app.mount("#app");
|
||||
|
||||
export const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [{ path: "/", component: App }]
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
@layer tailwind-base, primevue, tailwind-utilities;
|
||||
|
||||
@layer tailwind-base {
|
||||
@tailwind base;
|
||||
}
|
||||
|
||||
@layer tailwind-utilities {
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: white;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface-card);
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#[derive(Default)]
|
||||
pub struct TextListOption {
|
||||
pub hint: String,
|
||||
}
|
||||
|
||||
pub fn text_list_ui(
|
||||
ui: &mut egui::Ui,
|
||||
id: &str,
|
||||
texts: &mut Vec<String>,
|
||||
option: Option<TextListOption>,
|
||||
) {
|
||||
let option = option.unwrap_or_default();
|
||||
// convert text vec to (index, text) vec
|
||||
let mut add_new_item = false;
|
||||
let mut remove_idxs = vec![];
|
||||
|
||||
egui::Grid::new(id).max_col_width(200.0).show(ui, |ui| {
|
||||
for i in 0..texts.len() {
|
||||
egui::TextEdit::singleline(&mut texts[i])
|
||||
.hint_text(&option.hint)
|
||||
.show(ui);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("➖").clicked() {
|
||||
remove_idxs.push(i);
|
||||
}
|
||||
|
||||
if i == texts.len() - 1 {
|
||||
if ui.button("➕").clicked() {
|
||||
add_new_item = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
if texts.len() == 0 {
|
||||
if ui.button("➕").clicked() {
|
||||
add_new_item = true;
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
||||
let new_texts = texts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| !remove_idxs.contains(i))
|
||||
.map(|(_, t)| t.clone())
|
||||
.collect::<Vec<String>>();
|
||||
*texts = new_texts;
|
||||
|
||||
if add_new_item && texts.last().map(|t| !t.is_empty()).unwrap_or(true) {
|
||||
texts.push("".to_string());
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
//! Source code example of how to create your own widget.
|
||||
//! This is meant to be read as a tutorial, hence the plethora of comments.
|
||||
|
||||
/// iOS-style toggle switch:
|
||||
///
|
||||
/// ``` text
|
||||
/// _____________
|
||||
/// / /.....\
|
||||
/// | |.......|
|
||||
/// \_______\_____/
|
||||
/// ```
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// toggle_ui(ui, &mut my_bool);
|
||||
/// ```
|
||||
pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
// Widget code can be broken up in four steps:
|
||||
// 1. Decide a size for the widget
|
||||
// 2. Allocate space for it
|
||||
// 3. Handle interactions with the widget (if any)
|
||||
// 4. Paint the widget
|
||||
|
||||
// 1. Deciding widget size:
|
||||
// You can query the `ui` how much space is available,
|
||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
|
||||
// 2. Allocating space:
|
||||
// This is where we get a region of the screen assigned.
|
||||
// We also tell the Ui to sense clicks in the allocated region.
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
|
||||
// 3. Interact: Time to check for clicks!
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed(); // report back that the value changed
|
||||
}
|
||||
|
||||
// Attach some meta-data to the response which can be used by screen readers:
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
// 4. Paint!
|
||||
// Make sure we need to paint:
|
||||
if ui.is_rect_visible(rect) {
|
||||
// Let's ask for a simple animation from egui.
|
||||
// egui keeps track of changes in the boolean associated with the id and
|
||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
// We will follow the current style by asking
|
||||
// "how should something that is being interacted with be painted?".
|
||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
// Paint the circle, animating it from left to right with `how_on`:
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
||||
response
|
||||
}
|
||||
|
||||
/// Here is the same code again, but a bit more compact:
|
||||
#[allow(dead_code)]
|
||||
fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed();
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))`
|
||||
/// iOS-style toggle switch.
|
||||
///
|
||||
/// ## Example:
|
||||
/// ``` ignore
|
||||
/// ui.add(toggle(&mut my_bool));
|
||||
/// ```
|
||||
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
Reference in New Issue
Block a user