mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-14 05:37:23 +08:00
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:
@@ -2,12 +2,11 @@
|
||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||
import {computed, ref} from 'vue';
|
||||
import { Api } from 'easytier-frontend-lib'
|
||||
import {AutoComplete, Divider} from "primevue";
|
||||
import {AutoComplete, Divider, Button, Textarea} from "primevue";
|
||||
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
|
||||
|
||||
const apiHost = ref<string>(getInitialApiHost())
|
||||
const apiHostSuggestions = ref<Array<string>>([])
|
||||
const apiHostSearch = async (event: { query: string }) => {
|
||||
@@ -22,23 +21,46 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||
}
|
||||
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
const toml_config = ref<string>("Press 'Run Network' to generate TOML configuration");
|
||||
const toml_config = ref<string>("");
|
||||
const errorMessage = ref<string>("");
|
||||
|
||||
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||
saveApiHost(apiHost.value)
|
||||
errorMessage.value = "";
|
||||
api.value?.generate_config({
|
||||
config: config
|
||||
}).then((res) => {
|
||||
if (res.error) {
|
||||
toml_config.value = res.error;
|
||||
errorMessage.value = "Generation failed: " + res.error;
|
||||
} else if (res.toml_config) {
|
||||
toml_config.value = res.toml_config;
|
||||
} else {
|
||||
toml_config.value = "Api server returned an unexpected response";
|
||||
errorMessage.value = "Api server returned an unexpected response";
|
||||
}
|
||||
}).catch(err => {
|
||||
errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
};
|
||||
|
||||
const parseConfig = async () => {
|
||||
try {
|
||||
errorMessage.value = "";
|
||||
const res = await api.value?.parse_config({
|
||||
toml_config: toml_config.value
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
errorMessage.value = "Parse failed: " + res.error;
|
||||
} else if (res.config) {
|
||||
newNetworkConfig.value = res.config;
|
||||
} else {
|
||||
errorMessage.value = "API returned an unexpected response";
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = "Parse request failed: " + (e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -55,8 +77,17 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||
</div>
|
||||
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
||||
</div>
|
||||
<div class="sm:w-full md:w-1/2 p-4 bg-gray-100">
|
||||
<pre class="whitespace-pre-wrap">{{ toml_config }}</pre>
|
||||
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
|
||||
<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>
|
||||
<Textarea
|
||||
v-model="toml_config"
|
||||
spellcheck="false"
|
||||
class="w-full flex-grow p-2 bg-gray-100 whitespace-pre-wrap font-mono border-none focus:outline-none resize-none"
|
||||
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"
|
||||
></Textarea>
|
||||
<div class="mt-3 flex justify-center">
|
||||
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider} from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
||||
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider } from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
|
||||
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
@@ -33,6 +33,7 @@ const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const isEditing = ref(false);
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
const showConfigEditDialog = ref(false);
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||
@@ -103,7 +104,12 @@ const updateNetworkState = async (disabled: boolean) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
|
||||
if (disabled || !disabledNetworkConfig.value) {
|
||||
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
|
||||
} else if (disabledNetworkConfig.value) {
|
||||
await props.api?.delete_network(deviceId.value, disabledNetworkConfig.value.instance_id);
|
||||
await props.api?.run_network(deviceId.value, disabledNetworkConfig.value);
|
||||
}
|
||||
await loadNetworkInstanceIds();
|
||||
}
|
||||
|
||||
@@ -211,62 +217,97 @@ const loadDeviceInfo = async () => {
|
||||
}
|
||||
|
||||
const exportConfig = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
delete ret.instance_id;
|
||||
exportJsonFile(JSON.stringify(ret, null, 2),instanceId.value +'.json');
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let networkConfig = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
delete networkConfig.instance_id;
|
||||
let { toml_config: tomlConfig, error } = await props.api?.generate_config({
|
||||
config: networkConfig
|
||||
});
|
||||
if (error) {
|
||||
throw { response: { data: error } };
|
||||
}
|
||||
exportTomlFile(tomlConfig ?? '', instanceId.value + '.toml');
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const importConfig = () => {
|
||||
configFile.value.click();
|
||||
configFile.value.click();
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
const file = files ? files[0] : null;
|
||||
if (file) {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
const file = files ? files[0] : null;
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
let str = e.target?.result?.toString();
|
||||
if(str){
|
||||
const config = JSON.parse(str);
|
||||
if(config === null || typeof config !== "object"){
|
||||
throw new Error();
|
||||
}
|
||||
Object.assign(newNetworkConfig.value, config);
|
||||
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
let tomlConfig = e.target?.result?.toString();
|
||||
if (!tomlConfig) return;
|
||||
const resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
}
|
||||
|
||||
const config = resp.config;
|
||||
if (!config) return;
|
||||
|
||||
config.instance_id = newNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
|
||||
Object.assign(newNetworkConfig.value, resp.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 });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error.', life: 2000 });
|
||||
}
|
||||
configFile.value.value = null;
|
||||
configFile.value.value = null;
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
const exportJsonFile = (context: string, name: string) => {
|
||||
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/json' }));
|
||||
let link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.setAttribute('download', name);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
const exportTomlFile = (context: string, name: string) => {
|
||||
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/toml' }));
|
||||
let link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
link.setAttribute('download', name);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<string> => {
|
||||
let { toml_config: tomlConfig, error } = await props.api?.generate_config({ config });
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return tomlConfig ?? '';
|
||||
}
|
||||
|
||||
const saveConfig = async (tomlConfig: string): Promise<void> => {
|
||||
let resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
};
|
||||
const config = resp.config;
|
||||
if (!config) {
|
||||
throw new Error("Parsed config is empty");
|
||||
}
|
||||
config.instance_id = disabledNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
if (networkIsDisabled.value) {
|
||||
disabledNetworkConfig.value = config;
|
||||
} else {
|
||||
newNetworkConfig.value = config;
|
||||
}
|
||||
}
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
@@ -288,18 +329,23 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/json" ref="configFile"/>
|
||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
||||
:style="{ width: '55rem' }">
|
||||
<Dialog v-if="!networkIsDisabled" v-model:visible="showCreateNetworkDialog" modal
|
||||
:header="!isEditing ? 'Create New Network' : 'Edit Network'" :style="{ width: '55rem' }">
|
||||
<div class="flex flex-col">
|
||||
<div class="w-11/12 self-center ">
|
||||
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="w-11/12 self-center space-x-2">
|
||||
<Button @click="showConfigEditDialog = true" icon="pi pi-pen-to-square" label="Edit File" iconPos="right" />
|
||||
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</Dialog>
|
||||
<ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
|
||||
:cur-network="disabledNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" />
|
||||
<ConfigEditDialog v-else v-model:visible="showConfigEditDialog" :cur-network="newNetworkConfig"
|
||||
:generate-config="generateConfig" :save-config="saveConfig" />
|
||||
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
@@ -329,7 +375,7 @@ onUnmounted(() => {
|
||||
</Status>
|
||||
<Divider />
|
||||
<div class="text-center">
|
||||
<Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" />
|
||||
<Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user