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

View File

@@ -8,7 +8,8 @@ use easytier::proto::api::manage::{
WebClientServiceClientFactory,
};
use easytier::rpc_service::remote_client::{
ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager, Storage,
GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager,
Storage,
};
use easytier::{
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
@@ -249,6 +250,19 @@ fn load_configs(configs: Vec<NetworkConfig>, enabled_networks: Vec<String>) -> R
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"))]
fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(window) = app.get_webview_window("main") {
@@ -667,6 +681,7 @@ pub fn run() {
validate_config,
get_config,
load_configs,
get_network_metas,
])
.on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))]

View File

@@ -27,6 +27,7 @@ declare global {
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getEasytierVersion: typeof import('./composables/backend')['getEasytierVersion']
const getNetworkMetas: typeof import('./composables/backend')['getNetworkMetas']
const h: typeof import('vue')['h']
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
const inject: typeof import('vue')['inject']
@@ -139,6 +140,7 @@ declare module 'vue' {
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/backend')['getEasytierVersion']>
readonly getNetworkMetas: UnwrapRef<typeof import('./composables/backend')['getNetworkMetas']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>

View File

@@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core'
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'
type NetworkConfig = NetworkTypes.NetworkConfig
@@ -63,3 +64,7 @@ export async function sendConfigs() {
let autoStartInstIds = getAutoLaunchStatusAsync() ? JSON.parse(localStorage.getItem('autoStartInstIds') || '[]') : []
return await invoke('load_configs', { configs: networkList, enabledNetworks: autoStartInstIds })
}
export async function getNetworkMetas(instanceIds: string[]) {
return await invoke<GetNetworkMetasResponse>('get_network_metas', { instanceIds })
}

View File

@@ -40,5 +40,8 @@ export class GUIRemoteClient implements Api.RemoteClient {
return { error: e + "" };
}
}
async get_network_metas(instance_ids: string[]): Promise<Api.GetNetworkMetasResponse> {
return await backend.getNetworkMetas(instance_ids);
}
}

View File

@@ -1,5 +1,5 @@
<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 { useI18n } from 'vue-i18n';
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);
}
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 t = listInstanceIdResponse.value;
if (t) {
t.running_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({
get() {
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value);
return instanceList.value.find((instance) => instance.uuid === instanceId.value);
},
set(value: any) {
console.log("set instanceId", value);
@@ -62,6 +102,12 @@ const selectedInstanceId = computed({
watch(selectedInstanceId, async (newVal, oldVal) => {
if (newVal?.uuid !== oldVal?.uuid && (networkIsDisabled.value || isEditingNetwork.value)) {
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
return false;
}
if (isEditingNetwork.value) {
// editing network
return false;
}
return true;
})
@@ -96,7 +146,7 @@ const loadCurrentNetworkConfig = async () => {
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;
}
@@ -144,11 +194,18 @@ const confirmDeleteNetwork = (event: any) => {
};
const saveAndRunNewNetwork = async () => {
if (!currentNetworkConfig.value) {
return;
}
try {
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);
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) {
console.error(e);
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;
}
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 });
}
const newNetwork = async () => {
@@ -201,14 +262,17 @@ const loadNetworkInstanceIds = async () => {
}
const loadCurrentNetworkInfo = async () => {
if (!instanceId.value) {
if (!selectedInstanceId.value) {
return;
}
if (!needShowNetworkStatus.value) {
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 = {
instance_id: instanceId.value,
instance_id: selectedInstanceId.value.uuid,
running: network_info?.running ?? false,
error_msg: network_info?.error_msg ?? '',
detail: network_info,
@@ -365,13 +429,26 @@ onUnmounted(() => {
<!-- 网络选择 -->
<div class="flex-1 min-w-0">
<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')"
:pt="{ root: { class: 'network-select-container' } }">
:pt="{ root: { class: 'network-select-container' } }" :virtualScrollerOptions="{
lazy: true,
onLazyLoad: onLazyLoadNetworkMetas,
itemSize: 60,
delay: 50
}">
<template #value="slotProps">
<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">
<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>
<Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.value.uuid) ? 'success' : 'info'"
@@ -382,14 +459,20 @@ onUnmounted(() => {
</span>
</template>
<template #option="slotProps">
<div class="flex items-center content-center min-w-0">
<div class="mr-4 flex-col min-w-0 flex-1">
<span class="truncate block"> &nbsp; {{ slotProps.option.uuid }}</span>
<div class="flex flex-col items-start content-center max-w-full">
<div class="flex items-center min-w-0">
<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>
</template>
</Select>
<label class="network-label mr-2 font-medium" for="dd-inst-id">{{
@@ -601,4 +684,3 @@ onUnmounted(() => {
}
}
</style>

View File

@@ -26,6 +26,14 @@ export interface CollectNetworkInfoResponse {
}
}
export interface NetworkMeta {
instance_name: string;
}
export interface GetNetworkMetasResponse {
metas: Record<string, NetworkMeta>;
}
export interface RemoteClient {
validate_config(config: NetworkConfig): Promise<ValidateConfigResponse>;
run_network(config: NetworkConfig): Promise<undefined>;
@@ -37,4 +45,5 @@ export interface RemoteClient {
get_network_config(inst_id: string): Promise<NetworkConfig>;
generate_config(config: NetworkConfig): Promise<GenerateConfigResponse>;
parse_config(toml_config: string): Promise<ParseConfigResponse>;
get_network_metas(instance_ids: string[]): Promise<GetNetworkMetasResponse>;
}

View File

@@ -243,6 +243,12 @@ class WebRemoteClient implements Api.RemoteClient {
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;

View File

@@ -7,7 +7,7 @@ use easytier::launcher::NetworkConfig;
use easytier::proto::common::Void;
use easytier::proto::{api::manage::*, web::*};
use easytier::rpc_service::remote_client::{
ListNetworkInstanceIdsJsonResp, RemoteClientError, RemoteClientManager,
GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, RemoteClientError, RemoteClientManager,
};
use sea_orm::DbErr;
@@ -71,6 +71,11 @@ struct UpdateNetworkStateJsonReq {
disabled: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct GetNetworkMetasJsonReq {
instance_ids: Vec<uuid::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RemoveNetworkJsonReq {
inst_ids: Vec<uuid::Uuid>,
@@ -237,6 +242,23 @@ impl NetworkApi {
.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(
auth_session: AuthSession,
State(client_mgr): AppState,
@@ -298,5 +320,9 @@ impl NetworkApi {
"/api/v1/machines/:machine-id/networks/config/:inst-id",
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),
)
}
}

View File

@@ -189,6 +189,28 @@ where
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(
&self,
identify: T,
@@ -255,6 +277,16 @@ pub struct ListNetworkInstanceIdsJsonResp {
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> {
fn get_network_inst_id(&self) -> &str;
fn get_network_config(&self) -> Result<NetworkConfig, E>;