feat(web): add standalone WASM config generator (#2480)

Add a small Vite workspace that builds and publishes independently
of the dashboard. Reuse frontend-lib for the form and expose the
existing NetworkConfig conversions through wasm-bindgen.

Initialize the Aura theme in the standalone entry, detect the browser
language, and provide a persistent selector in the form header. Present
Generate Config and Copy Config as the page actions.

Keep the shared network secret field fluid so both form columns align.
Bundle the generator under dist/config-generator in the dashboard
artifact while preserving its standalone build output.

Build the optimized WASM module with the project and remove the
API-backed generator route from the dashboard.
This commit is contained in:
KKRainbow
2026-08-07 16:25:33 +08:00
committed by GitHub
parent 86222771c5
commit e31bde1836
22 changed files with 359 additions and 97 deletions
@@ -1,88 +0,0 @@
<script setup lang="ts">
import { NetworkTypes } from 'easytier-frontend-lib';
import { computed, ref } from 'vue';
import { AutoComplete, Divider, Button, Textarea } from "primevue";
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
import ApiClient from '../modules/api';
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
const apiHost = ref<string>(getInitialApiHost())
const apiHostSuggestions = ref<Array<string>>([])
const apiHostSearch = async (event: { query: string }) => {
apiHostSuggestions.value = [];
let hosts = cleanAndLoadApiHosts();
if (event.query) {
apiHostSuggestions.value.push(event.query);
}
hosts.forEach((host) => {
apiHostSuggestions.value.push(host.value);
});
}
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const toml_config = ref<string>("");
const errorMessage = ref<string>("");
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
saveApiHost(apiHost.value)
errorMessage.value = "";
api.value?.get_remote_client("").generate_config(config).then((res) => {
if (res.error) {
errorMessage.value = "Generation failed: " + res.error;
} else if (res.toml_config) {
toml_config.value = res.toml_config;
} else {
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?.get_remote_client("").parse_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>
<div class="flex items-center justify-center m-5">
<div class="sm:block md:flex w-full">
<div class="sm:w-full md:w-1/2 p-4">
<div class="flex flex-col">
<div class="w-full self-center ">
<label>ApiHost</label>
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
@complete="apiHostSearch" class="w-full" />
<Divider />
</div>
</div>
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
</div>
<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 whitespace-pre-wrap font-mono 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>
</template>
-5
View File
@@ -16,7 +16,6 @@ import DeviceManagement from './components/DeviceManagement.vue'
import Dashboard from './components/Dashboard.vue'
import DialogService from 'primevue/dialogservice';
import ToastService from 'primevue/toastservice';
import ConfigGenerator from './components/ConfigGenerator.vue'
const routes = [
{
@@ -67,10 +66,6 @@ const routes = [
return { name: 'login' }
}
}
},
{
path: '/config_generator',
component: ConfigGenerator,
}
]