Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990)

* add method to create NetworkConfig from TomlConfigLoader
* allow web export/import toml config file and gui edit toml config
* Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web
This commit is contained in:
Mg Pig
2025-06-15 23:41:42 +08:00
committed by GitHub
parent 40b5fe9a54
commit ed162c2e66
18 changed files with 738 additions and 80 deletions

View File

@@ -0,0 +1,103 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { NetworkConfig } from '../types/network';
import { Divider, Button, Dialog, Textarea } from 'primevue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
readonly: {
type: Boolean,
default: false,
},
generateConfig: {
type: Object as () => (config: NetworkConfig) => Promise<string>,
required: true,
},
saveConfig: {
type: Object as () => (config: string) => Promise<void>,
required: true,
},
})
const curNetwork = defineModel('curNetwork', {
type: Object as () => NetworkConfig | undefined,
required: true,
})
const visible = defineModel('visible', {
type: Boolean,
default: false,
})
watch([visible, curNetwork], async ([newVisible, newCurNetwork]) => {
if (!newVisible) {
tomlConfig.value = '';
return;
}
if (!newCurNetwork) {
tomlConfig.value = '';
return;
}
const config = newCurNetwork;
try {
errorMessage.value = '';
tomlConfig.value = await props.generateConfig(config);
} catch (e) {
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
tomlConfig.value = '';
}
})
onMounted(async () => {
if (!visible.value) {
return;
}
if (!curNetwork.value) {
tomlConfig.value = '';
return;
}
const config = curNetwork.value;
try {
tomlConfig.value = await props.generateConfig(config);
errorMessage.value = '';
} catch (e) {
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
tomlConfig.value = '';
}
});
const handleConfigSave = async () => {
if (props.readonly) return;
try {
await props.saveConfig(tomlConfig.value);
visible.value = false;
} catch (e) {
errorMessage.value = 'Failed to save config: ' + (e instanceof Error ? e.message : String(e));
}
};
const tomlConfig = ref<string>('')
const tomlConfigRows = ref<number>(1);
const errorMessage = ref<string>('');
watch(tomlConfig, (newValue) => {
tomlConfigRows.value = newValue.split('\n').length;
errorMessage.value = '';
});
</script>
<template>
<Dialog v-model:visible="visible" modal :header="t('config_file')" :style="{ width: '70%' }">
<pre v-if="errorMessage"
class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
<div class="flex w-full" style="max-height: 60vh; overflow-y: auto;">
<Textarea v-model="tomlConfig" class="w-full h-full font-mono flex flex-col resize-none" :rows="tomlConfigRows"
spellcheck="false" :readonly="props.readonly"></Textarea>
</div>
<Divider />
<div class="flex gap-2 justify-end">
<Button v-if="!props.readonly" type="button" :label="t('save')" @click="handleConfigSave" />
<Button type="button" :label="t('close')" @click="visible = false" />
</div>
</Dialog>
</template>

View File

@@ -1,2 +1,3 @@
export { default as Config } from './Config.vue';
export { default as Status } from './Status.vue';
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';

View File

@@ -1,7 +1,7 @@
import './style.css'
import type { App } from 'vue';
import { Config, Status } from "./components";
import { Config, Status, ConfigEditDialog } from "./components";
import Aura from '@primevue/themes/aura'
import PrimeVue from 'primevue/config'
@@ -41,10 +41,11 @@ export default {
});
app.component('Config', Config);
app.component('ConfigEditDialog', ConfigEditDialog);
app.component('Status', Status);
app.component('HumanEvent', HumanEvent);
app.directive('tooltip', vTooltip as any);
}
};
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils };

View File

@@ -51,7 +51,11 @@ dev_name_placeholder: 注意当多个网络同时使用相同的TUN接口名
off_text: 点击关闭
on_text: 点击开启
show_config: 显示配置
edit_config: 编辑配置文件
config_file: 配置文件
close: 关闭
save: 保存
config_saved: 配置已保存
use_latency_first: 延迟优先模式
my_node_info: 当前节点信息

View File

@@ -52,7 +52,11 @@ dev_name_placeholder: 'Note: When multiple networks use the same TUN interface n
off_text: Press to disable
on_text: Press to enable
show_config: Show Config
edit_config: Edit Config File
config_file: Config File
close: Close
save: Save
config_saved: Configuration saved
my_node_info: My Node Info
peer_count: Connected
upload: Upload

View File

@@ -47,6 +47,15 @@ export interface GenerateConfigResponse {
error?: string;
}
export interface ParseConfigRequest {
toml_config: string;
}
export interface ParseConfigResponse {
config?: NetworkConfig;
error?: string;
}
export class ApiClient {
private client: AxiosInstance;
private authFailedCb: Function | undefined;
@@ -215,6 +224,18 @@ export class ApiClient {
return { error: 'Unknown error: ' + error };
}
}
public async parse_config(config: ParseConfigRequest): Promise<ParseConfigResponse> {
try {
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', config);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return { error: error.response?.data };
}
return { error: 'Unknown error: ' + error };
}
}
}
export default ApiClient;