feat(gui): GUI add support to connect to config server (#1596)

This commit is contained in:
Mg Pig
2025-12-04 23:05:36 +08:00
committed by GitHub
parent 53f279f5ff
commit 0a718163fd
15 changed files with 458 additions and 105 deletions
+4
View File
@@ -33,12 +33,14 @@ declare global {
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
const initRpcConnection: typeof import('./composables/backend')['initRpcConnection']
const initService: typeof import('./composables/backend')['initService']
const initWebClient: typeof import('./composables/backend')['initWebClient']
const inject: typeof import('vue')['inject']
const isClientRunning: typeof import('./composables/backend')['isClientRunning']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const isWebClientConnected: typeof import('./composables/backend')['isWebClientConnected']
const listNetworkInstanceIds: typeof import('./composables/backend')['listNetworkInstanceIds']
const listenGlobalEvents: typeof import('./composables/event')['listenGlobalEvents']
const loadMode: typeof import('./composables/mode')['loadMode']
@@ -153,12 +155,14 @@ declare module 'vue' {
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
readonly initRpcConnection: UnwrapRef<typeof import('./composables/backend')['initRpcConnection']>
readonly initService: UnwrapRef<typeof import('./composables/backend')['initService']>
readonly initWebClient: UnwrapRef<typeof import('./composables/backend')['initWebClient']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isClientRunning: UnwrapRef<typeof import('./composables/backend')['isClientRunning']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly isWebClientConnected: UnwrapRef<typeof import('./composables/backend')['isWebClientConnected']>
readonly listNetworkInstanceIds: UnwrapRef<typeof import('./composables/backend')['listNetworkInstanceIds']>
readonly listenGlobalEvents: UnwrapRef<typeof import('./composables/event')['listenGlobalEvents']>
readonly loadMode: UnwrapRef<typeof import('./composables/mode')['loadMode']>
+11 -2
View File
@@ -11,6 +11,7 @@ interface ServiceOptions {
rpc_portal: string
file_log_level: string
file_log_dir: string
config_server?: string
}
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
@@ -67,9 +68,9 @@ export async function getConfig(instanceId: string) {
return await invoke<NetworkConfig>('get_config', { instanceId })
}
export async function sendConfigs() {
export async function sendConfigs(enabledNetworks: string[]) {
let networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
return await invoke('load_configs', { configs: networkList, enabledNetworks: [] })
return await invoke('load_configs', { configs: networkList, enabledNetworks })
}
export async function getNetworkMetas(instanceIds: string[]) {
@@ -95,3 +96,11 @@ export async function initRpcConnection(url?: string) {
export async function isClientRunning() {
return await invoke<boolean>('is_client_running')
}
export async function initWebClient(url?: string) {
return await invoke('init_web_client', { url })
}
export async function isWebClientConnected() {
return await invoke<boolean>('is_web_client_connected')
}
+13 -7
View File
@@ -1,8 +1,14 @@
interface NormalMode {
import { type } from '@tauri-apps/plugin-os';
export interface WebClientConfig {
config_server_url?: string
}
interface NormalMode extends WebClientConfig {
mode: 'normal'
}
export interface ServiceMode {
export interface ServiceMode extends WebClientConfig {
mode: 'service'
config_dir: string
rpc_portal: string
@@ -19,15 +25,15 @@ export function saveMode(mode: Mode) {
localStorage.setItem('app_mode', JSON.stringify(mode))
}
import { type } from '@tauri-apps/plugin-os';
export function loadMode(): Mode {
if (type() === 'android') {
return { mode: 'normal' };
}
const modeStr = localStorage.getItem('app_mode')
if (modeStr) {
return JSON.parse(modeStr) as Mode
let mode = JSON.parse(modeStr) as Mode
if (type() === 'android') {
return { ...mode, mode: 'normal' }
}
return mode
} else {
return { mode: 'normal' }
}
+114 -13
View File
@@ -5,15 +5,15 @@ import { type } from '@tauri-apps/plugin-os'
import { appLogDir } from '@tauri-apps/api/path'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { exit } from '@tauri-apps/plugin-process'
import { I18nUtils, RemoteManagement } from "easytier-frontend-lib"
import { I18nUtils, RemoteManagement, Utils } from "easytier-frontend-lib"
import type { MenuItem } from 'primevue/menuitem'
import { useTray } from '~/composables/tray'
import { GUIRemoteClient } from '~/modules/api'
import { useToast, useConfirm } from 'primevue'
import { loadMode, saveMode, type Mode } from '~/composables/mode'
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
import ModeSwitcher from '~/components/ModeSwitcher.vue'
import { getServiceStatus, type ServiceStatus } from '~/composables/backend'
import { getServiceStatus } from '~/composables/backend'
const { t, locale } = useI18n()
const confirm = useConfirm()
@@ -22,13 +22,13 @@ const modeDialogVisible = ref(false)
const currentMode = ref<Mode>({ mode: 'normal' })
const editingMode = ref<Mode>({ mode: 'normal' })
const isModeSaving = ref(false)
const serviceStatus = ref<ServiceStatus>('NotInstalled')
const manualDisconnect = ref(false)
const configServerDialogVisible = ref(false)
const configServerConnected = ref(false)
async function openModeDialog() {
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
if (editingMode.value.mode === 'service') {
serviceStatus.value = await getServiceStatus()
}
modeDialogVisible.value = true
}
@@ -84,6 +84,7 @@ async function onUninstallService() {
async function onStopService() {
isModeSaving.value = true
manualDisconnect.value = true
try {
await setServiceStatus(false)
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.stop_service_success'), life: 3000 })
@@ -99,11 +100,21 @@ async function onStopService() {
}
async function initWithMode(mode: Mode) {
const running_inst_ids = (await remoteClient.value.list_network_instance_ids().catch(() => undefined))?.running_inst_ids ?? []
if (currentMode.value.mode === 'service' && mode.mode !== 'service') {
let serviceStatus = await getServiceStatus()
if (serviceStatus === "Running") {
manualDisconnect.value = true
await setServiceStatus(false)
serviceStatus = await getServiceStatus()
for (let i = 0; i < 10; i++) { // macOS takes a while to stop the service
if (serviceStatus === "Stopped") {
break;
}
await new Promise(resolve => setTimeout(resolve, 100))
serviceStatus = await getServiceStatus()
}
}
if (serviceStatus === "Stopped") {
await initService(undefined)
@@ -127,11 +138,13 @@ async function initWithMode(mode: Mode) {
}
let serviceStatus = await getServiceStatus()
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
mode.config_server_url = mode.config_server_url || undefined
await initService({
config_dir: mode.config_dir,
file_log_dir: mode.file_log_dir,
file_log_level: mode.file_log_level,
rpc_portal: mode.rpc_portal,
config_server: mode.config_server_url,
})
serviceStatus = await getServiceStatus()
}
@@ -154,6 +167,11 @@ async function initWithMode(mode: Mode) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
await sendConfigs(running_inst_ids.map(Utils.UuidToStr))
if (mode.mode === 'normal') {
mode.config_server_url = mode.config_server_url || undefined
initWebClient(mode.config_server_url)
}
currentMode.value = mode
saveMode(mode)
clientRunning.value = await isClientRunning()
@@ -173,6 +191,10 @@ const clientRunning = ref(false);
watch(clientRunning, async (newVal, oldVal) => {
if (!newVal && oldVal) {
if (manualDisconnect.value) {
manualDisconnect.value = false
return
}
await reconnectClient()
}
})
@@ -187,9 +209,10 @@ onMounted(async () => {
console.error("Error checking client running status", e)
}
}, 1000)
return () => {
onUnmounted(() => {
clearInterval(timer)
}
})
})
async function reconnectClient() {
editingMode.value = JSON.parse(JSON.stringify(loadMode()));
@@ -262,6 +285,12 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
command: openModeDialog,
visible: () => type() !== 'android',
},
{
label: () => `${t('config-server.title')}${t('config-server.' + configServerConnectionStatus.value)}`,
icon: 'pi pi-globe',
command: openConfigServerDialog,
visible: () => ["normal", "service"].includes(currentMode.value.mode),
},
{
key: 'logging_menu',
label: () => t('logging'),
@@ -286,7 +315,6 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
async function connectRpcClient(url?: string) {
await initRpcConnection(url)
await sendConfigs()
console.log("easytier rpc connection established")
}
@@ -300,9 +328,66 @@ onMounted(async () => {
}
}
const unlisten = await listenGlobalEvents()
return () => {
onUnmounted(() => {
unlisten()
})
})
async function openConfigServerDialog() {
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
configServerDialogVisible.value = true
}
async function onConfigServerSave() {
if (JSON.stringify(currentMode.value) === JSON.stringify(editingMode.value)) {
configServerDialogVisible.value = false
return;
}
if (editingMode.value.mode === 'service') {
await new Promise<void>((resolve, reject) => {
confirm.require({
message: t('config-server.update_service_confirm'),
icon: 'pi pi-exclamation-triangle',
rejectProps: {
label: t('web.common.cancel'),
severity: 'secondary',
outlined: true
},
acceptProps: {
label: t('web.common.confirm'),
},
accept: async () => {
resolve()
},
reject: () => {
reject()
}
});
})
}
console.log("Saving config server url", (editingMode.value as WebClientConfig).config_server_url)
await onModeSave();
configServerDialogVisible.value = false
}
onMounted(() => {
const timer = setInterval(async () => {
if (currentMode.value.mode !== 'normal') return;
if (!currentMode.value.config_server_url) return;
configServerConnected.value = await isWebClientConnected();
}, 1000)
onUnmounted(() => {
clearInterval(timer)
})
})
const configServerConnectionStatus = computed(() => {
if (currentMode.value.mode !== 'normal') {
return 'unknown'
}
if (!currentMode.value.config_server_url) {
return 'disconnected'
}
return configServerConnected.value ? 'connected' : 'connecting'
})
</script>
@@ -319,10 +404,26 @@ onMounted(async () => {
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onModeSave" autofocus :loading="isModeSaving" />
</template>
</Dialog>
<Dialog v-model:visible="configServerDialogVisible" modal :header="t('config-server.title')"
:style="{ width: '50vw' }">
<div class="flex flex-col gap-3">
<label for="config-server-address">{{ t('config-server.address') }}</label>
<InputText id="config-server-address" v-model="(editingMode as WebClientConfig).config_server_url"
:placeholder="t('config-server.address_placeholder')" />
<small class="p-text-secondary whitespace-pre-wrap">{{ t('config-server.description') }}</small>
</div>
<template #footer>
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="configServerDialogVisible = false" text />
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onConfigServerSave" autofocus
:loading="isModeSaving" />
</template>
</Dialog>
<Menu ref="log_menu" :model="log_menu_items_popup" :popup="true" />
<RemoteManagement v-if="clientRunning" class="flex-1 overflow-y-auto" :api="remoteClient"
:pause-auto-refresh="isModeSaving" v-bind:instance-id="instanceId" />
:pause-auto-refresh="isModeSaving" v-model:instance-id="instanceId" />
<div v-else class="empty-state flex-1 flex flex-col items-center py-12">
<i class="pi pi-server text-5xl text-secondary mb-4 opacity-50"></i>
<div class="text-xl text-center font-medium mb-3">{{ t('client.not_running') }}
@@ -331,7 +432,7 @@ onMounted(async () => {
iconPos="left" />
</div>
<Menubar :model="setting_menu_items" breakpoint="560px">
<Menubar :model="setting_menu_items" breakpoint="795px">
<template #item="{ item, props }">
<a v-if="item.key === 'logging_menu'" v-bind="props.action" @click="toggle_log_menu">
<span :class="item.icon" />