feat(ui): Display network names and optimize list loading (#1503)

This commit is contained in:
Mg Pig
2025-10-22 13:40:36 +08:00
committed by GitHub
parent eba9504fc2
commit bbe8f9f810
9 changed files with 204 additions and 24 deletions
+16 -1
View File
@@ -8,7 +8,8 @@ use easytier::proto::api::manage::{
WebClientServiceClientFactory, WebClientServiceClientFactory,
}; };
use easytier::rpc_service::remote_client::{ use easytier::rpc_service::remote_client::{
ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager, Storage, GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager,
Storage,
}; };
use easytier::{ use easytier::{
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader}, common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
@@ -249,6 +250,19 @@ fn load_configs(configs: Vec<NetworkConfig>, enabled_networks: Vec<String>) -> R
Ok(()) Ok(())
} }
#[tauri::command]
async fn get_network_metas(
app: AppHandle,
instance_ids: Vec<uuid::Uuid>,
) -> Result<GetNetworkMetasResponse, String> {
CLIENT_MANAGER
.get()
.unwrap()
.handle_get_network_metas(app, instance_ids)
.await
.map_err(|e| e.to_string())
}
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) { fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
@@ -667,6 +681,7 @@ pub fn run() {
validate_config, validate_config,
get_config, get_config,
load_configs, load_configs,
get_network_metas,
]) ])
.on_window_event(|_win, event| match event { .on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
+2
View File
@@ -27,6 +27,7 @@ declare global {
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const getEasytierVersion: typeof import('./composables/backend')['getEasytierVersion'] const getEasytierVersion: typeof import('./composables/backend')['getEasytierVersion']
const getNetworkMetas: typeof import('./composables/backend')['getNetworkMetas']
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService'] const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
@@ -139,6 +140,7 @@ declare module 'vue' {
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']> readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/backend')['getEasytierVersion']> readonly getEasytierVersion: UnwrapRef<typeof import('./composables/backend')['getEasytierVersion']>
readonly getNetworkMetas: UnwrapRef<typeof import('./composables/backend')['getNetworkMetas']>
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']> readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
readonly inject: UnwrapRef<typeof import('vue')['inject']> readonly inject: UnwrapRef<typeof import('vue')['inject']>
+5
View File
@@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { Api, type NetworkTypes } from 'easytier-frontend-lib' import { Api, type NetworkTypes } from 'easytier-frontend-lib'
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
import { getAutoLaunchStatusAsync } from '~/modules/auto_launch' import { getAutoLaunchStatusAsync } from '~/modules/auto_launch'
type NetworkConfig = NetworkTypes.NetworkConfig type NetworkConfig = NetworkTypes.NetworkConfig
@@ -63,3 +64,7 @@ export async function sendConfigs() {
let autoStartInstIds = getAutoLaunchStatusAsync() ? JSON.parse(localStorage.getItem('autoStartInstIds') || '[]') : [] let autoStartInstIds = getAutoLaunchStatusAsync() ? JSON.parse(localStorage.getItem('autoStartInstIds') || '[]') : []
return await invoke('load_configs', { configs: networkList, enabledNetworks: autoStartInstIds }) return await invoke('load_configs', { configs: networkList, enabledNetworks: autoStartInstIds })
} }
export async function getNetworkMetas(instanceIds: string[]) {
return await invoke<GetNetworkMetasResponse>('get_network_metas', { instanceIds })
}
+3
View File
@@ -40,5 +40,8 @@ export class GUIRemoteClient implements Api.RemoteClient {
return { error: e + "" }; return { error: e + "" };
} }
} }
async get_network_metas(instance_ids: string[]): Promise<Api.GetNetworkMetasResponse> {
return await backend.getNetworkMetas(instance_ids);
}
} }
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Button, ConfirmPopup, Divider, IftaLabel, Menu, Message, Select, Tag, useConfirm, useToast } from 'primevue'; import { Button, ConfirmPopup, Divider, IftaLabel, Menu, Message, Select, Tag, useConfirm, useToast, type VirtualScrollerLazyEvent } from 'primevue';
import { computed, onMounted, onUnmounted, Ref, ref, watch } from 'vue'; import { computed, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import * as Api from '../modules/api'; import * as Api from '../modules/api';
@@ -37,22 +37,62 @@ const isRunning = (instanceId: string) => {
return listInstanceIdResponse.value?.running_inst_ids.map(Utils.UuidToStr).includes(instanceId); return listInstanceIdResponse.value?.running_inst_ids.map(Utils.UuidToStr).includes(instanceId);
} }
const instanceIdList = computed(() => { const networkMetaCache = ref<Record<string, Api.NetworkMeta>>({});
const loadNetworkMetas = async (instanceIds: string[]) => {
const missingIds = instanceIds.filter(id => !networkMetaCache.value[id]);
if (missingIds.length === 0) return;
try {
const response = await props.api.get_network_metas(missingIds);
Object.assign(networkMetaCache.value, response.metas);
} catch (e) {
console.error("Failed to load network metas", e);
}
};
const onLazyLoadNetworkMetas = async (event: VirtualScrollerLazyEvent) => {
const instanceIds = instanceList.value
.slice(event.first, event.last + 1)
.map(item => item.uuid);
await loadNetworkMetas(instanceIds);
};
const instanceList = ref<Array<{ uuid: string; meta?: Api.NetworkMeta }>>([]);
const updateInstanceList = () => {
let insts = new Set<string>(); let insts = new Set<string>();
let t = listInstanceIdResponse.value; let t = listInstanceIdResponse.value;
if (t) { if (t) {
t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u))); t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u))); t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
} }
let options = Array.from(insts).map((instance: string) => {
return { uuid: instance }; const newList = Array.from(insts).map((instance: string) => {
return {
uuid: instance,
meta: networkMetaCache.value[instance]
};
}); });
return options;
if (JSON.stringify(newList) !== JSON.stringify(instanceList.value)) {
instanceList.value = newList;
}
}
watch(listInstanceIdResponse, updateInstanceList, { deep: false });
watch(networkMetaCache, updateInstanceList, { deep: true });
watch(instanceList, async (newVal) => {
if (newVal) {
const instanceIds = new Set(newVal.map(item => item.uuid));
Object.keys(networkMetaCache.value).forEach(id => {
if (!instanceIds.has(id)) {
delete networkMetaCache.value[id];
}
});
}
}); });
const selectedInstanceId = computed({ const selectedInstanceId = computed({
get() { get() {
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value); return instanceList.value.find((instance) => instance.uuid === instanceId.value);
}, },
set(value: any) { set(value: any) {
console.log("set instanceId", value); console.log("set instanceId", value);
@@ -62,6 +102,12 @@ const selectedInstanceId = computed({
watch(selectedInstanceId, async (newVal, oldVal) => { watch(selectedInstanceId, async (newVal, oldVal) => {
if (newVal?.uuid !== oldVal?.uuid && (networkIsDisabled.value || isEditingNetwork.value)) { if (newVal?.uuid !== oldVal?.uuid && (networkIsDisabled.value || isEditingNetwork.value)) {
await loadCurrentNetworkConfig(); await loadCurrentNetworkConfig();
} else {
await loadCurrentNetworkInfo();
}
if (newVal?.uuid && !networkMetaCache.value[newVal.uuid]) {
await loadNetworkMetas([newVal.uuid]);
} }
}); });
@@ -74,6 +120,10 @@ const needShowNetworkStatus = computed(() => {
// network is disabled // network is disabled
return false; return false;
} }
if (isEditingNetwork.value) {
// editing network
return false;
}
return true; return true;
}) })
@@ -96,7 +146,7 @@ const loadCurrentNetworkConfig = async () => {
return; return;
} }
let ret = await props.api.get_network_config(selectedInstanceId.value.uuid); let ret = await props.api.get_network_config(selectedInstanceId.value!.uuid);
currentNetworkConfig.value = ret; currentNetworkConfig.value = ret;
} }
@@ -144,11 +194,18 @@ const confirmDeleteNetwork = (event: any) => {
}; };
const saveAndRunNewNetwork = async () => { const saveAndRunNewNetwork = async () => {
if (!currentNetworkConfig.value) {
return;
}
try { try {
await props.api.delete_network(instanceId.value!); await props.api.delete_network(instanceId.value!);
let ret = await props.api.run_network(currentNetworkConfig.value!!); let ret = await props.api.run_network(currentNetworkConfig.value);
console.debug("saveAndRunNewNetwork", ret); console.debug("saveAndRunNewNetwork", ret);
selectedInstanceId.value = { uuid: currentNetworkConfig.value!.instance_id };
delete networkMetaCache.value[currentNetworkConfig.value.instance_id];
await loadNetworkMetas([currentNetworkConfig.value.instance_id]);
selectedInstanceId.value = { uuid: currentNetworkConfig.value.instance_id };
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 }); toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 });
@@ -164,6 +221,10 @@ const saveNetworkConfig = async () => {
return; return;
} }
await props.api.save_config(currentNetworkConfig.value); await props.api.save_config(currentNetworkConfig.value);
delete networkMetaCache.value[currentNetworkConfig.value.instance_id];
await loadNetworkMetas([currentNetworkConfig.value.instance_id]);
toast.add({ severity: 'success', summary: t("web.common.success"), detail: t("web.device_management.config_saved"), life: 2000 }); toast.add({ severity: 'success', summary: t("web.common.success"), detail: t("web.device_management.config_saved"), life: 2000 });
} }
const newNetwork = async () => { const newNetwork = async () => {
@@ -201,14 +262,17 @@ const loadNetworkInstanceIds = async () => {
} }
const loadCurrentNetworkInfo = async () => { const loadCurrentNetworkInfo = async () => {
if (!instanceId.value) { if (!selectedInstanceId.value) {
return;
}
if (!needShowNetworkStatus.value) {
return; return;
} }
let network_info = await props.api.get_network_info(instanceId.value); let network_info = await props.api.get_network_info(selectedInstanceId.value.uuid);
curNetworkInfo.value = { curNetworkInfo.value = {
instance_id: instanceId.value, instance_id: selectedInstanceId.value.uuid,
running: network_info?.running ?? false, running: network_info?.running ?? false,
error_msg: network_info?.error_msg ?? '', error_msg: network_info?.error_msg ?? '',
detail: network_info, detail: network_info,
@@ -365,13 +429,26 @@ onUnmounted(() => {
<!-- 网络选择 --> <!-- 网络选择 -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<IftaLabel class="w-full"> <IftaLabel class="w-full">
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" class="w-full" <Select v-model="selectedInstanceId" :options="instanceList" optionLabel="uuid" class="w-full"
inputId="dd-inst-id" :placeholder="t('web.device_management.select_network')" inputId="dd-inst-id" :placeholder="t('web.device_management.select_network')"
:pt="{ root: { class: 'network-select-container' } }"> :pt="{ root: { class: 'network-select-container' } }" :virtualScrollerOptions="{
lazy: true,
onLazyLoad: onLazyLoadNetworkMetas,
itemSize: 60,
delay: 50
}">
<template #value="slotProps"> <template #value="slotProps">
<div v-if="slotProps.value" class="flex items-center content-center min-w-0"> <div v-if="slotProps.value" class="flex items-center content-center min-w-0">
<div class="mr-4 flex-col min-w-0 flex-1"> <div class="mr-4 flex-col min-w-0 flex-1">
<span class="truncate block"> &nbsp; {{ slotProps.value.uuid }}</span> <span class="truncate block">
&nbsp;
<span v-if="slotProps.value.meta">
{{ slotProps.value.meta.instance_name }} ({{ slotProps.value.uuid }})
</span>
<span v-else>
{{ slotProps.value.uuid }}
</span>
</span>
</div> </div>
<Tag class="my-auto leading-3 shrink-0" <Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.value.uuid) ? 'success' : 'info'" :severity="isRunning(slotProps.value.uuid) ? 'success' : 'info'"
@@ -382,13 +459,19 @@ onUnmounted(() => {
</span> </span>
</template> </template>
<template #option="slotProps"> <template #option="slotProps">
<div class="flex items-center content-center min-w-0"> <div class="flex flex-col items-start content-center max-w-full">
<div class="mr-4 flex-col min-w-0 flex-1"> <div class="flex items-center min-w-0">
<span class="truncate block"> &nbsp; {{ slotProps.option.uuid }}</span> <div class="mr-4 min-w-0 flex-1">
<span class="truncate block">{{ t('network_name') }}: {{
slotProps.option.meta.instance_name }}</span>
</div>
<Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.option.uuid) ? 'success' : 'info'"
:value="t(isRunning(slotProps.option.uuid) ? 'network_running' : 'network_stopped')" />
</div>
<div class="max-w-full overflow-hidden text-ellipsis text-gray-500">
{{ slotProps.option.uuid }}
</div> </div>
<Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.option.uuid) ? 'success' : 'info'"
:value="t(isRunning(slotProps.option.uuid) ? 'network_running' : 'network_stopped')" />
</div> </div>
</template> </template>
</Select> </Select>
@@ -601,4 +684,3 @@ onUnmounted(() => {
} }
} }
</style> </style>
@@ -26,6 +26,14 @@ export interface CollectNetworkInfoResponse {
} }
} }
export interface NetworkMeta {
instance_name: string;
}
export interface GetNetworkMetasResponse {
metas: Record<string, NetworkMeta>;
}
export interface RemoteClient { export interface RemoteClient {
validate_config(config: NetworkConfig): Promise<ValidateConfigResponse>; validate_config(config: NetworkConfig): Promise<ValidateConfigResponse>;
run_network(config: NetworkConfig): Promise<undefined>; run_network(config: NetworkConfig): Promise<undefined>;
@@ -37,4 +45,5 @@ export interface RemoteClient {
get_network_config(inst_id: string): Promise<NetworkConfig>; get_network_config(inst_id: string): Promise<NetworkConfig>;
generate_config(config: NetworkConfig): Promise<GenerateConfigResponse>; generate_config(config: NetworkConfig): Promise<GenerateConfigResponse>;
parse_config(toml_config: string): Promise<ParseConfigResponse>; parse_config(toml_config: string): Promise<ParseConfigResponse>;
get_network_metas(instance_ids: string[]): Promise<GetNetworkMetasResponse>;
} }
+6
View File
@@ -243,6 +243,12 @@ class WebRemoteClient implements Api.RemoteClient {
return { error: 'Unknown error: ' + error }; return { error: 'Unknown error: ' + error };
} }
} }
async get_network_metas(instance_ids: string[]): Promise<Api.GetNetworkMetasResponse> {
const response = await this.client.post<any, Api.GetNetworkMetasResponse>(`/machines/${this.machine_id}/networks/metas`, {
instance_ids: instance_ids
});
return response;
}
} }
export default ApiClient; export default ApiClient;
+27 -1
View File
@@ -7,7 +7,7 @@ use easytier::launcher::NetworkConfig;
use easytier::proto::common::Void; use easytier::proto::common::Void;
use easytier::proto::{api::manage::*, web::*}; use easytier::proto::{api::manage::*, web::*};
use easytier::rpc_service::remote_client::{ use easytier::rpc_service::remote_client::{
ListNetworkInstanceIdsJsonResp, RemoteClientError, RemoteClientManager, GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, RemoteClientError, RemoteClientManager,
}; };
use sea_orm::DbErr; use sea_orm::DbErr;
@@ -71,6 +71,11 @@ struct UpdateNetworkStateJsonReq {
disabled: bool, disabled: bool,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct GetNetworkMetasJsonReq {
instance_ids: Vec<uuid::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RemoveNetworkJsonReq { struct RemoveNetworkJsonReq {
inst_ids: Vec<uuid::Uuid>, inst_ids: Vec<uuid::Uuid>,
@@ -237,6 +242,23 @@ impl NetworkApi {
.map_err(convert_error) .map_err(convert_error)
} }
async fn handle_get_network_metas(
auth_session: AuthSession,
State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>,
Json(payload): Json<GetNetworkMetasJsonReq>,
) -> Result<Json<GetNetworkMetasResponse>, HttpHandleError> {
Ok(Json(
client_mgr
.handle_get_network_metas(
(Self::get_user_id(&auth_session)?, machine_id),
payload.instance_ids,
)
.await
.map_err(convert_error)?,
))
}
async fn handle_save_network_config( async fn handle_save_network_config(
auth_session: AuthSession, auth_session: AuthSession,
State(client_mgr): AppState, State(client_mgr): AppState,
@@ -298,5 +320,9 @@ impl NetworkApi {
"/api/v1/machines/:machine-id/networks/config/:inst-id", "/api/v1/machines/:machine-id/networks/config/:inst-id",
get(Self::handle_get_network_config).put(Self::handle_save_network_config), get(Self::handle_get_network_config).put(Self::handle_save_network_config),
) )
.route(
"/api/v1/machines/:machine-id/networks/metas",
post(Self::handle_get_network_metas),
)
} }
} }
+32
View File
@@ -189,6 +189,28 @@ where
Ok(()) Ok(())
} }
async fn handle_get_network_metas(
&self,
identify: T,
inst_ids: Vec<uuid::Uuid>,
) -> Result<GetNetworkMetasResponse, RemoteClientError<E>> {
let mut metas = std::collections::HashMap::new();
for instance_id in inst_ids {
let config = self
.handle_get_network_config(identify.clone(), instance_id)
.await?;
metas.insert(
instance_id,
NetworkMeta {
instance_name: config.network_name.unwrap_or_default(),
},
);
}
Ok(GetNetworkMetasResponse { metas })
}
async fn handle_save_network_config( async fn handle_save_network_config(
&self, &self,
identify: T, identify: T,
@@ -255,6 +277,16 @@ pub struct ListNetworkInstanceIdsJsonResp {
disabled_inst_ids: Vec<crate::proto::common::Uuid>, disabled_inst_ids: Vec<crate::proto::common::Uuid>,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct NetworkMeta {
instance_name: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct GetNetworkMetasResponse {
metas: std::collections::HashMap<uuid::Uuid, NetworkMeta>,
}
pub trait PersistentConfig<E> { pub trait PersistentConfig<E> {
fn get_network_inst_id(&self) -> &str; fn get_network_inst_id(&self) -> &str;
fn get_network_config(&self) -> Result<NetworkConfig, E>; fn get_network_config(&self) -> Result<NetworkConfig, E>;