mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-16 06:37:23 +08:00
refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component (#1489)
* refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component * feat(gui): Add network config saving and refactor RemoteManagement
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, ConfirmPopup, Divider, IftaLabel, Menu, Select, useConfirm, useToast } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Button, ConfirmPopup, Divider, IftaLabel, Menu, Message, Select, Tag, useConfirm, useToast } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import * as Api from '../modules/api';
|
||||
import { RemoteClient } from '../modules/api';
|
||||
import * as Utils from '../modules/utils';
|
||||
import * as NetworkTypes from '../types/network';
|
||||
import { type MenuItem } from 'primevue/menuitem';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
api: RemoteClient;
|
||||
api: Api.RemoteClient;
|
||||
newConfigGenerator?: () => NetworkTypes.NetworkConfig;
|
||||
}>();
|
||||
|
||||
@@ -27,15 +27,16 @@ const configFile = ref();
|
||||
|
||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const isEditing = ref(false);
|
||||
// const showCreateNetworkDialog = ref(false);
|
||||
const showConfigEditDialog = ref(false);
|
||||
const isCreatingNetwork = ref(false); // Flag to indicate if we're in network creation mode
|
||||
const editingNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
const isEditingNetwork = ref(false); // Flag to indicate if we're in network editing mode
|
||||
const currentNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
|
||||
|
||||
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||
|
||||
const isRunning = (instanceId: string) => {
|
||||
return listInstanceIdResponse.value?.running_inst_ids.map(Utils.UuidToStr).includes(instanceId);
|
||||
}
|
||||
|
||||
const instanceIdList = computed(() => {
|
||||
let insts = new Set<string>();
|
||||
let t = listInstanceIdResponse.value;
|
||||
@@ -59,7 +60,7 @@ const selectedInstanceId = computed({
|
||||
}
|
||||
});
|
||||
watch(selectedInstanceId, async (newVal, oldVal) => {
|
||||
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
|
||||
if (newVal?.uuid !== oldVal?.uuid && (networkIsDisabled.value || isEditingNetwork.value)) {
|
||||
await loadCurrentNetworkConfig();
|
||||
}
|
||||
});
|
||||
@@ -144,12 +145,10 @@ const confirmDeleteNetwork = (event: any) => {
|
||||
|
||||
const saveAndRunNewNetwork = async () => {
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await props.api.delete_network(instanceId.value!);
|
||||
}
|
||||
let ret = await props.api.run_network(editingNetworkConfig.value);
|
||||
await props.api.delete_network(instanceId.value!);
|
||||
let ret = await props.api.run_network(currentNetworkConfig.value!!);
|
||||
console.debug("saveAndRunNewNetwork", ret);
|
||||
selectedInstanceId.value = { uuid: editingNetworkConfig.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 });
|
||||
@@ -157,18 +156,26 @@ const saveAndRunNewNetwork = async () => {
|
||||
}
|
||||
emits('update');
|
||||
// showCreateNetworkDialog.value = false;
|
||||
isCreatingNetwork.value = false; // Exit creation mode after successful network creation
|
||||
isEditingNetwork.value = false; // Exit creation mode after successful network creation
|
||||
}
|
||||
|
||||
const newNetwork = () => {
|
||||
editingNetworkConfig.value = props.newConfigGenerator?.() ?? NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
isEditing.value = false;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation mode instead
|
||||
const saveNetworkConfig = async () => {
|
||||
if (!currentNetworkConfig.value) {
|
||||
return;
|
||||
}
|
||||
await props.api.save_config(currentNetworkConfig.value);
|
||||
toast.add({ severity: 'success', summary: t("web.common.success"), detail: t("web.device_management.config_saved"), life: 2000 });
|
||||
}
|
||||
const newNetwork = async () => {
|
||||
const newNetworkConfig = props.newConfigGenerator?.() ?? NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
await props.api.save_config(newNetworkConfig);
|
||||
selectedInstanceId.value = { uuid: newNetworkConfig.instance_id };
|
||||
currentNetworkConfig.value = newNetworkConfig;
|
||||
await loadNetworkInstanceIds();
|
||||
}
|
||||
|
||||
const cancelNetworkCreation = () => {
|
||||
isCreatingNetwork.value = false;
|
||||
const cancelEditNetwork = () => {
|
||||
isEditingNetwork.value = false;
|
||||
}
|
||||
|
||||
const editNetwork = async () => {
|
||||
@@ -177,14 +184,11 @@ const editNetwork = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
isEditing.value = true;
|
||||
|
||||
try {
|
||||
let ret = await props.api.get_network_config(instanceId.value!);
|
||||
console.debug("editNetwork", ret);
|
||||
editingNetworkConfig.value = ret;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation mode instead
|
||||
currentNetworkConfig.value = ret;
|
||||
isEditingNetwork.value = true; // Switch to editing mode instead
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to edit network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
@@ -194,7 +198,6 @@ const editNetwork = async () => {
|
||||
|
||||
const loadNetworkInstanceIds = async () => {
|
||||
listInstanceIdResponse.value = await props.api.list_network_instance_ids();
|
||||
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
|
||||
}
|
||||
|
||||
const loadCurrentNetworkInfo = async () => {
|
||||
@@ -202,13 +205,12 @@ const loadCurrentNetworkInfo = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await props.api.get_network_info(instanceId.value);
|
||||
let network_info = ret[instanceId.value];
|
||||
let network_info = await props.api.get_network_info(instanceId.value);
|
||||
|
||||
curNetworkInfo.value = {
|
||||
instance_id: instanceId.value,
|
||||
running: network_info.running,
|
||||
error_msg: network_info.error_msg,
|
||||
running: network_info?.running ?? false,
|
||||
error_msg: network_info?.error_msg ?? '',
|
||||
detail: network_info,
|
||||
} as NetworkTypes.NetworkInstance;
|
||||
}
|
||||
@@ -220,9 +222,8 @@ const exportConfig = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
let networkConfig = await props.api.get_network_config(instanceId.value!);
|
||||
delete networkConfig.instance_id;
|
||||
let { toml_config: tomlConfig, error } = await props.api.generate_config(networkConfig);
|
||||
const { instance_id, ...networkConfig } = await props.api.get_network_config(instanceId.value!);
|
||||
let { toml_config: tomlConfig, error } = await props.api.generate_config(networkConfig as NetworkTypes.NetworkConfig);
|
||||
if (error) {
|
||||
throw { response: { data: error } };
|
||||
}
|
||||
@@ -255,9 +256,8 @@ const handleFileUpload = (event: Event) => {
|
||||
const config = resp.config;
|
||||
if (!config) return;
|
||||
|
||||
config.instance_id = editingNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
|
||||
Object.assign(editingNetworkConfig.value, resp.config);
|
||||
config.instance_id = currentNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
currentNetworkConfig.value = config;
|
||||
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error: ' + error, life: 2000 });
|
||||
@@ -288,7 +288,7 @@ const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<strin
|
||||
return tomlConfig ?? '';
|
||||
}
|
||||
|
||||
const saveConfig = async (tomlConfig: string): Promise<void> => {
|
||||
const syncTomlConfig = async (tomlConfig: string): Promise<void> => {
|
||||
let resp = await props.api.parse_config(tomlConfig);
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
@@ -298,11 +298,7 @@ const saveConfig = async (tomlConfig: string): Promise<void> => {
|
||||
throw new Error("Parsed config is empty");
|
||||
}
|
||||
config.instance_id = currentNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
if (networkIsDisabled.value) {
|
||||
currentNetworkConfig.value = config;
|
||||
} else {
|
||||
editingNetworkConfig.value = config;
|
||||
}
|
||||
currentNetworkConfig.value = config;
|
||||
}
|
||||
|
||||
// 响应式屏幕宽度
|
||||
@@ -313,10 +309,11 @@ const updateScreenWidth = () => {
|
||||
|
||||
// 菜单引用和菜单项
|
||||
const menuRef = ref();
|
||||
const actionMenu = ref([
|
||||
const actionMenu: Ref<MenuItem[]> = ref([
|
||||
{
|
||||
label: t('web.device_management.edit_network'),
|
||||
icon: 'pi pi-pencil',
|
||||
visible: () => !(networkIsDisabled.value ?? true),
|
||||
command: () => editNetwork()
|
||||
},
|
||||
{
|
||||
@@ -370,7 +367,31 @@ onUnmounted(() => {
|
||||
<IftaLabel class="w-full">
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" 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' } }">
|
||||
<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"> {{ slotProps.value.uuid }}</span>
|
||||
</div>
|
||||
<Tag class="my-auto leading-3 shrink-0"
|
||||
:severity="isRunning(slotProps.value.uuid) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.value.uuid) ? 'network_running' : 'network_stopped')" />
|
||||
</div>
|
||||
<span v-else>
|
||||
{{ slotProps.placeholder }}
|
||||
</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"> {{ slotProps.option.uuid }}</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>
|
||||
</template>
|
||||
</Select>
|
||||
<label class="network-label mr-2 font-medium" for="dd-inst-id">{{
|
||||
t('web.device_management.network') }}</label>
|
||||
</IftaLabel>
|
||||
@@ -379,23 +400,23 @@ onUnmounted(() => {
|
||||
<!-- 简化的按钮区域 - 无论屏幕大小都显示 -->
|
||||
<div class="flex gap-2 shrink-0 button-container items-center">
|
||||
<!-- Create/Cancel button based on state -->
|
||||
<Button v-if="!isCreatingNetwork" @click="newNetwork" icon="pi pi-plus"
|
||||
<Button v-if="!isEditingNetwork" @click="newNetwork" icon="pi pi-plus"
|
||||
:label="screenWidth > 640 ? t('web.device_management.create_new') : undefined"
|
||||
:class="['create-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.create_network') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="primary" />
|
||||
|
||||
<Button v-else @click="cancelNetworkCreation" icon="pi pi-times"
|
||||
:label="screenWidth > 640 ? t('web.device_management.cancel_creation') : undefined"
|
||||
<Button v-else @click="cancelEditNetwork" icon="pi pi-times"
|
||||
:label="screenWidth > 640 ? t('web.device_management.cancel_edit') : undefined"
|
||||
:class="['cancel-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.cancel_creation') : undefined"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.cancel_edit') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="secondary" />
|
||||
|
||||
<!-- More actions menu -->
|
||||
<Menu ref="menuRef" :model="actionMenu" :popup="true" />
|
||||
<Button v-if="!isCreatingNetwork && selectedInstanceId" icon="pi pi-ellipsis-v"
|
||||
<Button v-if="!isEditingNetwork && selectedInstanceId" icon="pi pi-ellipsis-v"
|
||||
class="p-button-rounded flex items-center justify-center" severity="help"
|
||||
style="width: 3rem !important; height: 3rem !important; font-size: 1.2rem"
|
||||
@click="menuRef.toggle($event)" :aria-label="t('web.device_management.more_actions')"
|
||||
@@ -407,11 +428,10 @@ onUnmounted(() => {
|
||||
<!-- Main Content Area -->
|
||||
<div class="network-content bg-surface-0 p-4 rounded-lg shadow-sm">
|
||||
<!-- Network Creation Form -->
|
||||
<div v-if="isCreatingNetwork" class="network-creation-container">
|
||||
<div v-if="isEditingNetwork || networkIsDisabled" class="network-creation-container">
|
||||
<div class="network-creation-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-plus-circle text-primary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ isEditing ? t('web.device_management.edit_network') :
|
||||
t('web.device_management.create_network') }}</h2>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.edit_network') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 flex-wrap justify-start mb-3">
|
||||
@@ -419,11 +439,13 @@ onUnmounted(() => {
|
||||
:label="t('web.device_management.edit_as_file')" iconPos="left" severity="secondary" />
|
||||
<Button @click="importConfig" icon="pi pi-upload" :label="t('web.device_management.import_config')"
|
||||
iconPos="left" severity="help" />
|
||||
<Button v-if="networkIsDisabled" @click="saveNetworkConfig" icon="pi pi-save"
|
||||
:label="t('web.device_management.save_config')" iconPos="left" severity="success" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Config :cur-network="editingNetworkConfig" @run-network="saveAndRunNewNetwork"></Config>
|
||||
<Config :cur-network="currentNetworkConfig" @run-network="saveAndRunNewNetwork"></Config>
|
||||
</div>
|
||||
|
||||
<!-- Network Status (for running networks) -->
|
||||
@@ -433,7 +455,10 @@ onUnmounted(() => {
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.network_status') }}</h2>
|
||||
</div>
|
||||
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo" class="mb-4"></Status>
|
||||
<Status v-if="(curNetworkInfo?.error_msg ?? '') === ''" v-bind:cur-network-inst="curNetworkInfo"
|
||||
class="mb-4">
|
||||
</Status>
|
||||
<Message v-else severity="error" class="mb-4">{{ curNetworkInfo?.error_msg }}</Message>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<Button @click="updateNetworkState(true)" :label="t('web.device_management.disable_network')"
|
||||
@@ -441,23 +466,6 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Configuration (for disabled networks) -->
|
||||
<div v-else-if="networkIsDisabled" class="network-config-container">
|
||||
<div class="network-config-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-cog text-secondary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.network_configuration') }}</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="currentNetworkConfig" class="mb-4">
|
||||
<Config :cur-network="currentNetworkConfig" @run-network="updateNetworkState(false)" />
|
||||
</div>
|
||||
<div v-else class="network-loading-placeholder text-center py-8">
|
||||
<i class="pi pi-spin pi-spinner text-3xl text-primary mb-3"></i>
|
||||
<div class="text-xl text-secondary">{{ t('web.device_management.loading_network_configuration') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state flex flex-col items-center py-12">
|
||||
<i class="pi pi-sitemap text-5xl text-secondary mb-4 opacity-50"></i>
|
||||
@@ -475,8 +483,8 @@ onUnmounted(() => {
|
||||
<!-- <ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
|
||||
:cur-network="currentNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" /> -->
|
||||
|
||||
<ConfigEditDialog v-model:visible="showConfigEditDialog" :cur-network="editingNetworkConfig"
|
||||
:generate-config="generateConfig" :save-config="saveConfig" />
|
||||
<ConfigEditDialog v-model:visible="showConfigEditDialog" :cur-network="currentNetworkConfig"
|
||||
:generate-config="generateConfig" :save-config="syncTomlConfig" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -592,4 +600,5 @@ onUnmounted(() => {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
|
||||
@@ -289,9 +289,11 @@ web:
|
||||
network: 网络
|
||||
select_network: 选择网络
|
||||
create_network: 创建网络
|
||||
cancel_creation: 取消创建
|
||||
cancel_edit: 取消编辑
|
||||
more_actions: 更多操作
|
||||
edit_as_file: 编辑为文件
|
||||
save_config: 保存配置
|
||||
config_saved: 配置已保存
|
||||
import_config: 导入配置
|
||||
create_new: 创建新网络
|
||||
network_status: 网络状态
|
||||
|
||||
@@ -289,9 +289,11 @@ web:
|
||||
network: Network
|
||||
select_network: Select Network
|
||||
create_network: Create Network
|
||||
cancel_creation: Cancel Creation
|
||||
cancel_edit: Cancel Edit
|
||||
more_actions: More Actions
|
||||
edit_as_file: Edit as File
|
||||
save_config: Save Config
|
||||
config_saved: Config Saved
|
||||
import_config: Import Config
|
||||
create_new: Create New Network
|
||||
network_status: Network Status
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UUID } from './utils';
|
||||
import { NetworkConfig } from '../types/network';
|
||||
import { NetworkConfig, NetworkInstanceRunningInfo } from '../types/network';
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
@@ -20,14 +20,21 @@ export interface ParseConfigResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface CollectNetworkInfoResponse {
|
||||
info: {
|
||||
map: Record<string, NetworkInstanceRunningInfo | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RemoteClient {
|
||||
validate_config(config: any): Promise<ValidateConfigResponse>;
|
||||
run_network(config: any): Promise<undefined>;
|
||||
get_network_info(inst_id: string): Promise<any>;
|
||||
validate_config(config: NetworkConfig): Promise<ValidateConfigResponse>;
|
||||
run_network(config: NetworkConfig): Promise<undefined>;
|
||||
get_network_info(inst_id: string): Promise<NetworkInstanceRunningInfo | undefined>;
|
||||
list_network_instance_ids(): Promise<ListNetworkInstanceIdResponse>;
|
||||
delete_network(inst_id: string): Promise<undefined>;
|
||||
update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined>;
|
||||
get_network_config(inst_id: string): Promise<any>;
|
||||
save_config(config: NetworkConfig): Promise<undefined>;
|
||||
get_network_config(inst_id: string): Promise<NetworkConfig>;
|
||||
generate_config(config: NetworkConfig): Promise<GenerateConfigResponse>;
|
||||
parse_config(toml_config: string): Promise<ParseConfigResponse>;
|
||||
}
|
||||
Reference in New Issue
Block a user