mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-14 05:37:23 +08:00
refactor(web): Refactor web logic to extract reusable remote client management module (#1465)
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { Card, Password, Button } from 'primevue';
|
||||
import { Api } from 'easytier-frontend-lib';
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const dialogRef = inject<any>('dialogRef');
|
||||
|
||||
const api = computed<Api.ApiClient>(() => dialogRef.value.data.api);
|
||||
const api = computed<ApiClient>(() => dialogRef.value.data.api);
|
||||
|
||||
const password = ref('');
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||
import { computed, ref } from 'vue';
|
||||
import { Api } from 'easytier-frontend-lib'
|
||||
import { AutoComplete, Divider, Button, Textarea } from "primevue";
|
||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
|
||||
|
||||
const apiHost = ref<string>(getInitialApiHost())
|
||||
const apiHostSuggestions = ref<Array<string>>([])
|
||||
@@ -27,9 +27,7 @@ const errorMessage = ref<string>("");
|
||||
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||
saveApiHost(apiHost.value)
|
||||
errorMessage.value = "";
|
||||
api.value?.generate_config({
|
||||
config: config
|
||||
}).then((res) => {
|
||||
api.value?.get_remote_client("").generate_config(config).then((res) => {
|
||||
if (res.error) {
|
||||
errorMessage.value = "Generation failed: " + res.error;
|
||||
} else if (res.toml_config) {
|
||||
@@ -45,9 +43,7 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||
const parseConfig = async () => {
|
||||
try {
|
||||
errorMessage.value = "";
|
||||
const res = await api.value?.parse_config({
|
||||
toml_config: toml_config.value
|
||||
});
|
||||
const res = await api.value?.get_remote_client("").parse_config(toml_config.value);
|
||||
|
||||
if (res.error) {
|
||||
errorMessage.value = "Parse failed: " + res.error;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { Card, useToast } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
import { Utils } from 'easytier-frontend-lib';
|
||||
import ApiClient, { Summary } from '../modules/api';
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
api: ApiClient,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const summary = ref<Api.Summary | undefined>(undefined);
|
||||
const summary = ref<Summary | undefined>(undefined);
|
||||
|
||||
const loadSummary = async () => {
|
||||
const resp = await props.api?.get_summary();
|
||||
|
||||
@@ -3,9 +3,10 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Button, Drawer, ProgressSpinner, useToast, InputSwitch, Popover, Dropdown, Toolbar } from 'primevue';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
import { Utils } from 'easytier-frontend-lib';
|
||||
import DeviceDetails from './DeviceDetails.vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -15,7 +16,7 @@ declare const window: Window & typeof globalThis;
|
||||
const vTooltip = Tooltip;
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
api: ApiClient,
|
||||
});
|
||||
|
||||
const detailPopover = ref();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { IftaLabel, Select, Button, ConfirmPopup, useConfirm, useToast, Divider, Menu } from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
|
||||
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { NetworkTypes, Utils, Api, RemoteManagement } from 'easytier-frontend-lib';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
api: Api.ApiClient;
|
||||
api: ApiClient;
|
||||
deviceList: Array<Utils.DeviceInfo> | undefined;
|
||||
}>();
|
||||
|
||||
@@ -16,7 +14,6 @@ const emits = defineEmits(['update']);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const deviceId = computed<string>(() => {
|
||||
return route.params.deviceId as string;
|
||||
@@ -30,469 +27,29 @@ const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
|
||||
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
|
||||
});
|
||||
|
||||
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 newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||
|
||||
const instanceIdList = computed(() => {
|
||||
let insts = new Set(deviceInfo.value?.running_network_instances || []);
|
||||
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 };
|
||||
});
|
||||
return options;
|
||||
});
|
||||
|
||||
const selectedInstanceId = computed({
|
||||
get() {
|
||||
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value);
|
||||
return instanceId.value;
|
||||
},
|
||||
set(value: any) {
|
||||
console.log("set instanceId", value);
|
||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value.uuid } });
|
||||
set(value: string) {
|
||||
console.log("selectedInstanceId", value);
|
||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value } });
|
||||
}
|
||||
});
|
||||
|
||||
const needShowNetworkStatus = computed(() => {
|
||||
if (!selectedInstanceId.value) {
|
||||
// nothing selected
|
||||
return false;
|
||||
}
|
||||
if (networkIsDisabled.value) {
|
||||
// network is disabled
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
const remoteClient = computed<Api.RemoteClient>(() => props.api.get_remote_client(deviceId.value));
|
||||
|
||||
const networkIsDisabled = computed(() => {
|
||||
if (!selectedInstanceId.value) {
|
||||
return false;
|
||||
}
|
||||
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
|
||||
});
|
||||
|
||||
watch(selectedInstanceId, async (newVal, oldVal) => {
|
||||
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
|
||||
await loadDisabledNetworkConfig();
|
||||
}
|
||||
});
|
||||
|
||||
const disabledNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
|
||||
|
||||
const loadDisabledNetworkConfig = async () => {
|
||||
disabledNetworkConfig.value = undefined;
|
||||
|
||||
if (!deviceId.value || !selectedInstanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await props.api?.get_network_config(deviceId.value, selectedInstanceId.value.uuid);
|
||||
disabledNetworkConfig.value = ret;
|
||||
const newConfigGenerator = () => {
|
||||
const config = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
config.hostname = deviceInfo.value?.hostname;
|
||||
return config;
|
||||
}
|
||||
|
||||
const updateNetworkState = async (disabled: boolean) => {
|
||||
if (!deviceId.value || !selectedInstanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
const confirm = useConfirm();
|
||||
const confirmDeleteNetwork = (event: any) => {
|
||||
confirm.require({
|
||||
target: event.currentTarget,
|
||||
message: 'Do you want to delete this network?',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectProps: {
|
||||
label: 'Cancel',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Delete',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
emits('update');
|
||||
},
|
||||
reject: () => {
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
||||
// let ret = await props.api?.validate_config(deviceId.value, newNetworkConfig.value);
|
||||
// console.log("verifyNetworkConfig", ret);
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
const createNewNetwork = async () => {
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await props.api?.delete_network(deviceId.value, instanceId.value);
|
||||
}
|
||||
let ret = await props.api?.run_network(deviceId.value, newNetworkConfig.value);
|
||||
console.debug("createNewNetwork", ret);
|
||||
} 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 });
|
||||
return;
|
||||
}
|
||||
emits('update');
|
||||
showCreateNetworkDialog.value = false;
|
||||
isCreatingNetwork.value = false; // Exit creation mode after successful network creation
|
||||
}
|
||||
|
||||
const newNetwork = () => {
|
||||
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
newNetworkConfig.value.hostname = deviceInfo.value?.hostname;
|
||||
isEditing.value = false;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation mode instead
|
||||
}
|
||||
|
||||
const cancelNetworkCreation = () => {
|
||||
isCreatingNetwork.value = false;
|
||||
}
|
||||
|
||||
const editNetwork = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
isEditing.value = true;
|
||||
|
||||
try {
|
||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
console.debug("editNetwork", ret);
|
||||
newNetworkConfig.value = ret;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation 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 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loadNetworkInstanceIds = async () => {
|
||||
if (!deviceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
listInstanceIdResponse.value = await props.api?.list_deivce_instance_ids(deviceId.value);
|
||||
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
|
||||
}
|
||||
|
||||
const loadDeviceInfo = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await props.api?.get_network_info(deviceId.value, instanceId.value);
|
||||
let device_info = ret[instanceId.value];
|
||||
|
||||
curNetworkInfo.value = {
|
||||
instance_id: instanceId.value,
|
||||
running: device_info.running,
|
||||
error_msg: device_info.error_msg,
|
||||
detail: device_info,
|
||||
} as NetworkTypes.NetworkInstance;
|
||||
}
|
||||
|
||||
const exportConfig = async () => {
|
||||
if (!deviceId.value || !instanceId.value) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', 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();
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
const file = files ? files[0] : null;
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
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 });
|
||||
}
|
||||
configFile.value.value = null;
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式屏幕宽度
|
||||
const screenWidth = ref(window.innerWidth);
|
||||
const updateScreenWidth = () => {
|
||||
screenWidth.value = window.innerWidth;
|
||||
};
|
||||
|
||||
// 菜单引用和菜单项
|
||||
const menuRef = ref();
|
||||
const actionMenu = ref([
|
||||
{
|
||||
label: t('web.device_management.edit_network'),
|
||||
icon: 'pi pi-pencil',
|
||||
command: () => editNetwork()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.export_config'),
|
||||
icon: 'pi pi-download',
|
||||
command: () => exportConfig()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.delete_network'),
|
||||
icon: 'pi pi-trash',
|
||||
class: 'p-error',
|
||||
command: () => confirmDeleteNetwork(new Event('click'))
|
||||
}
|
||||
]);
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
|
||||
// 添加屏幕尺寸监听
|
||||
window.addEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
|
||||
// 移除屏幕尺寸监听
|
||||
window.removeEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="device-management">
|
||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
|
||||
<!-- 网络选择和操作按钮始终在同一行 -->
|
||||
<div class="network-header bg-surface-50 p-3 rounded-lg shadow-sm mb-1">
|
||||
<div class="flex flex-row justify-between items-center gap-2" style="align-items: center;">
|
||||
<!-- 网络选择 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<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' } }" />
|
||||
<label class="network-label mr-2 font-medium" for="dd-inst-id">{{
|
||||
t('web.device_management.network') }}</label>
|
||||
</IftaLabel>
|
||||
</div>
|
||||
|
||||
<!-- 简化的按钮区域 - 无论屏幕大小都显示 -->
|
||||
<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"
|
||||
: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"
|
||||
: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"
|
||||
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"
|
||||
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')"
|
||||
:tooltip="t('web.device_management.more_actions')" tooltipOptions="{ position: 'bottom' }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 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>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 flex-wrap justify-start mb-3">
|
||||
<Button @click="showConfigEditDialog = true" icon="pi pi-file-edit"
|
||||
: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" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</div>
|
||||
|
||||
<!-- Network Status (for running networks) -->
|
||||
<div v-else-if="needShowNetworkStatus" class="network-status-container">
|
||||
<div class="network-status-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-chart-line text-primary text-xl"></i>
|
||||
<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>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<Button @click="updateNetworkState(true)" :label="t('web.device_management.disable_network')"
|
||||
severity="warning" icon="pi pi-power-off" iconPos="left" />
|
||||
</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="disabledNetworkConfig" class="mb-4">
|
||||
<Config :cur-network="disabledNetworkConfig" @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>
|
||||
<div class="text-xl text-center font-medium mb-3">{{ t('web.device_management.no_network_selected') }}
|
||||
</div>
|
||||
<p class="text-secondary text-center mb-6 max-w-md">
|
||||
{{ t('web.device_management.select_existing_network_or_create_new') }}
|
||||
</p>
|
||||
<Button @click="newNetwork" :label="t('web.device_management.create_network')" icon="pi pi-plus"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keep only the config edit dialogs -->
|
||||
<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" />
|
||||
</div>
|
||||
<RemoteManagement :api="remoteClient" v-model:instance-id="selectedInstanceId"
|
||||
:new-config-generator="newConfigGenerator" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,9 +3,10 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib';
|
||||
import { I18nUtils } from 'easytier-frontend-lib';
|
||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient, { Credential, RegisterData } from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -13,7 +14,7 @@ defineProps<{
|
||||
isRegistering: boolean;
|
||||
}>();
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
@@ -28,7 +29,7 @@ const captchaSrc = computed(() => api.value.captcha_url());
|
||||
const onSubmit = async () => {
|
||||
// Add your login logic here
|
||||
saveApiHost(apiHost.value);
|
||||
const credential: Api.Credential = { username: username.value, password: password.value, };
|
||||
const credential: Credential = { username: username.value, password: password.value, };
|
||||
let ret = await api.value?.login(credential);
|
||||
if (ret.success) {
|
||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||
@@ -43,8 +44,8 @@ const onSubmit = async () => {
|
||||
|
||||
const onRegister = async () => {
|
||||
saveApiHost(apiHost.value);
|
||||
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
|
||||
const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
|
||||
let ret = await api.value?.register(registerReq);
|
||||
if (ret.success) {
|
||||
toast.add({ severity: 'success', summary: 'Register Success', detail: ret.message, life: 2000 });
|
||||
@@ -108,12 +109,12 @@ onMounted(() => {
|
||||
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="register-username" class="block text-sm font-medium">{{ t('web.login.username')
|
||||
}}</label>
|
||||
}}</label>
|
||||
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="register-password" class="block text-sm font-medium">{{ t('web.login.password')
|
||||
}}</label>
|
||||
}}</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
:feedback="false" class="w-full" />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib'
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
||||
import { Button, TieredMenu } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
@@ -7,13 +7,14 @@ import { useDialog } from 'primevue/usedialog';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import Icon from '../assets/easytier.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const api = computed<Api.ApiClient | undefined>(() => {
|
||||
const api = computed<ApiClient | undefined>(() => {
|
||||
try {
|
||||
return new Api.ApiClient(atob(route.params.apiHost as string), () => {
|
||||
return new ApiClient(atob(route.params.apiHost as string), () => {
|
||||
router.push({ name: 'login' });
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
246
easytier-web/frontend/src/modules/api.ts
Normal file
246
easytier-web/frontend/src/modules/api.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { Md5 } from 'ts-md5'
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
// 定义接口返回的数据结构
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 定义请求体数据结构
|
||||
export interface Credential {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
credentials: Credential;
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
device_count: number;
|
||||
}
|
||||
|
||||
export interface ListNetworkInstanceIdResponse {
|
||||
running_inst_ids: Array<Utils.UUID>,
|
||||
disabled_inst_ids: Array<Utils.UUID>,
|
||||
}
|
||||
|
||||
export interface GenerateConfigRequest {
|
||||
config: NetworkTypes.NetworkConfig;
|
||||
}
|
||||
|
||||
export interface GenerateConfigResponse {
|
||||
toml_config?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigRequest {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigResponse {
|
||||
config?: NetworkTypes.NetworkConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
private authFailedCb: Function | undefined;
|
||||
|
||||
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl + '/api/v1',
|
||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
this.authFailedCb = authFailedCb;
|
||||
|
||||
// 添加请求拦截器
|
||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
return config;
|
||||
}, (error: any) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.debug('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
let response: AxiosResponse = error.response;
|
||||
if (response.status == 401 && this.authFailedCb) {
|
||||
console.error('Unauthorized:', response.data);
|
||||
this.authFailedCb();
|
||||
} else {
|
||||
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
||||
console.error('Response Error:', error.response.data);
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 请求已发出,但是没有收到响应
|
||||
console.error('Request Error:', error.request);
|
||||
} else {
|
||||
// 发生了一些问题导致请求未发出
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
// 注册
|
||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||
try {
|
||||
data.credentials.password = Md5.hashStr(data.credentials.password);
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
console.log("register response:", response);
|
||||
return { success: true, message: 'Register success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { success: false, message: 'Failed to register, error: ' + JSON.stringify(error.response?.data), };
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
// 登录
|
||||
public async login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
data.password = Md5.hashStr(data.password);
|
||||
const response = await this.client.post<any>('/auth/login', data);
|
||||
console.log("login response:", response);
|
||||
return { success: true, message: 'Login success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
return { success: false, message: 'Invalid username or password', };
|
||||
} else {
|
||||
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
|
||||
}
|
||||
}
|
||||
return { success: false, message: 'Unknown error, error: ' + error, };
|
||||
}
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
await this.client.get('/auth/logout');
|
||||
if (this.authFailedCb) {
|
||||
this.authFailedCb();
|
||||
}
|
||||
}
|
||||
|
||||
public async change_password(new_password: string) {
|
||||
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
|
||||
}
|
||||
|
||||
public async check_login_status() {
|
||||
try {
|
||||
await this.client.get('/auth/check_login_status');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async list_session() {
|
||||
const response = await this.client.get('/sessions');
|
||||
return response;
|
||||
}
|
||||
|
||||
public async list_machines(): Promise<Array<any>> {
|
||||
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
|
||||
return response.machines;
|
||||
}
|
||||
|
||||
public async get_summary(): Promise<Summary> {
|
||||
const response = await this.client.get<any, Summary>('/summary');
|
||||
return response;
|
||||
}
|
||||
|
||||
public captcha_url() {
|
||||
return this.client.defaults.baseURL + '/auth/captcha';
|
||||
}
|
||||
|
||||
public get_remote_client(machine_id: string): Api.RemoteClient {
|
||||
return new WebRemoteClient(machine_id, this.client);
|
||||
}
|
||||
}
|
||||
|
||||
class WebRemoteClient implements Api.RemoteClient {
|
||||
private machine_id: string;
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(machine_id: string, client: AxiosInstance) {
|
||||
this.machine_id = machine_id;
|
||||
this.client = client;
|
||||
}
|
||||
async validate_config(config: any): Promise<Api.ValidateConfigResponse> {
|
||||
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${this.machine_id}/validate-config`, {
|
||||
config: config,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
async run_network(config: any): Promise<undefined> {
|
||||
await this.client.post<string>(`/machines/${this.machine_id}/networks`, {
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
async get_network_info(inst_id: string): Promise<any> {
|
||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + this.machine_id + '/networks/info/' + inst_id);
|
||||
return response.info.map;
|
||||
}
|
||||
async list_network_instance_ids(): Promise<Api.ListNetworkInstanceIdResponse> {
|
||||
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + this.machine_id + '/networks');
|
||||
return response;
|
||||
}
|
||||
async delete_network(inst_id: string): Promise<undefined> {
|
||||
await this.client.delete<string>(`/machines/${this.machine_id}/networks/${inst_id}`);
|
||||
}
|
||||
async update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined> {
|
||||
await this.client.put<string>('/machines/' + this.machine_id + '/networks/' + inst_id, {
|
||||
disabled: disabled,
|
||||
});
|
||||
}
|
||||
async get_network_config(inst_id: string): Promise<any> {
|
||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + this.machine_id + '/networks/config/' + inst_id);
|
||||
return response;
|
||||
}
|
||||
async generate_config(config: NetworkTypes.NetworkConfig): Promise<Api.GenerateConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { error: error.response?.data };
|
||||
}
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
}
|
||||
async parse_config(toml_config: string): Promise<Api.ParseConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', toml_config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { error: error.response?.data };
|
||||
}
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
Reference in New Issue
Block a user