refactor(web): Refactor web logic to extract reusable remote client management module (#1465)
This commit is contained in:
@@ -25,11 +25,9 @@
|
|||||||
"floating-vue": "^5.2",
|
"floating-vue": "^5.2",
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.3.9",
|
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"ts-md5": "^1.3.1",
|
"ts-md5": "^1.3.1",
|
||||||
"uuid": "^11.0.2",
|
"uuid": "^11.0.2",
|
||||||
"vue": "^3.5.12",
|
|
||||||
"vue-chartjs": "^5.3.2",
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-i18n": "^10.0.4"
|
"vue-i18n": "^10.0.4"
|
||||||
},
|
},
|
||||||
@@ -46,5 +44,9 @@
|
|||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-dts": "^4.3.0",
|
"vite-plugin-dts": "^4.3.0",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.12",
|
||||||
|
"primevue": "^4.3.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
595
easytier-web/frontend-lib/src/components/RemoteManagement.vue
Normal file
595
easytier-web/frontend-lib/src/components/RemoteManagement.vue
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, ConfirmPopup, Divider, IftaLabel, Menu, Select, useConfirm, useToast } from 'primevue';
|
||||||
|
import { computed, onMounted, onUnmounted, 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';
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
api: RemoteClient;
|
||||||
|
newConfigGenerator?: () => NetworkTypes.NetworkConfig;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const instanceId = defineModel('instanceId', {
|
||||||
|
type: String as () => string | undefined,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['update']);
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
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 currentNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
|
||||||
|
|
||||||
|
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||||
|
|
||||||
|
const instanceIdList = computed(() => {
|
||||||
|
let insts = new Set<string>();
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
set(value: any) {
|
||||||
|
console.log("set instanceId", value);
|
||||||
|
instanceId.value = value ? value.uuid : undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(selectedInstanceId, async (newVal, oldVal) => {
|
||||||
|
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
|
||||||
|
await loadCurrentNetworkConfig();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const needShowNetworkStatus = computed(() => {
|
||||||
|
if (!selectedInstanceId.value) {
|
||||||
|
// nothing selected
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (networkIsDisabled.value) {
|
||||||
|
// network is disabled
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
|
||||||
|
const networkIsDisabled = computed(() => {
|
||||||
|
if (!selectedInstanceId.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
|
||||||
|
});
|
||||||
|
watch(networkIsDisabled, async (newVal, oldVal) => {
|
||||||
|
if (newVal !== oldVal && newVal === true) {
|
||||||
|
await loadCurrentNetworkConfig();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadCurrentNetworkConfig = async () => {
|
||||||
|
currentNetworkConfig.value = undefined;
|
||||||
|
|
||||||
|
if (!selectedInstanceId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = await props.api.get_network_config(selectedInstanceId.value.uuid);
|
||||||
|
currentNetworkConfig.value = ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateNetworkState = async (disabled: boolean) => {
|
||||||
|
if (!selectedInstanceId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled || !currentNetworkConfig.value) {
|
||||||
|
await props.api.update_network_instance_state(selectedInstanceId.value.uuid, disabled);
|
||||||
|
} else if (currentNetworkConfig.value) {
|
||||||
|
await props.api.delete_network(currentNetworkConfig.value.instance_id);
|
||||||
|
await props.api.run_network(currentNetworkConfig.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(instanceId.value!);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
emits('update');
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAndRunNewNetwork = async () => {
|
||||||
|
try {
|
||||||
|
if (isEditing.value) {
|
||||||
|
await props.api.delete_network(instanceId.value!);
|
||||||
|
}
|
||||||
|
let ret = await props.api.run_network(editingNetworkConfig.value);
|
||||||
|
console.debug("saveAndRunNewNetwork", ret);
|
||||||
|
selectedInstanceId.value = { uuid: editingNetworkConfig.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 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emits('update');
|
||||||
|
// showCreateNetworkDialog.value = false;
|
||||||
|
isCreatingNetwork.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 cancelNetworkCreation = () => {
|
||||||
|
isCreatingNetwork.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editNetwork = async () => {
|
||||||
|
if (!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(instanceId.value!);
|
||||||
|
console.debug("editNetwork", ret);
|
||||||
|
editingNetworkConfig.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 () => {
|
||||||
|
listInstanceIdResponse.value = await props.api.list_network_instance_ids();
|
||||||
|
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCurrentNetworkInfo = async () => {
|
||||||
|
if (!instanceId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = await props.api.get_network_info(instanceId.value);
|
||||||
|
let network_info = ret[instanceId.value];
|
||||||
|
|
||||||
|
curNetworkInfo.value = {
|
||||||
|
instance_id: instanceId.value,
|
||||||
|
running: network_info.running,
|
||||||
|
error_msg: network_info.error_msg,
|
||||||
|
detail: network_info,
|
||||||
|
} as NetworkTypes.NetworkInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportConfig = async () => {
|
||||||
|
if (!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(instanceId.value!);
|
||||||
|
delete networkConfig.instance_id;
|
||||||
|
let { toml_config: tomlConfig, error } = await props.api.generate_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(tomlConfig);
|
||||||
|
if (resp.error) {
|
||||||
|
throw resp.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = resp.config;
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
config.instance_id = editingNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||||
|
|
||||||
|
Object.assign(editingNetworkConfig.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(tomlConfig);
|
||||||
|
if (resp.error) {
|
||||||
|
throw resp.error;
|
||||||
|
};
|
||||||
|
const config = resp.config;
|
||||||
|
if (!config) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式屏幕宽度
|
||||||
|
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(), loadCurrentNetworkInfo()]);
|
||||||
|
} 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="editingNetworkConfig" @run-network="saveAndRunNewNetwork"></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="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>
|
||||||
|
<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="currentNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" /> -->
|
||||||
|
|
||||||
|
<ConfigEditDialog v-model:visible="showConfigEditDialog" :cur-network="editingNetworkConfig"
|
||||||
|
:generate-config="generateConfig" :save-config="saveConfig" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.device-management {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.button-container {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-button {
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单样式定制 */
|
||||||
|
:deep(.p-menu) {
|
||||||
|
min-width: 12rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-menu .p-menuitem) {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-menu .p-menuitem-link) {
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-menu .p-menuitem-icon) {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-menu .p-menuitem.p-error .p-menuitem-text,
|
||||||
|
.p-menu .p-menuitem.p-error .p-menuitem-icon) {
|
||||||
|
color: var(--red-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-menu .p-menuitem:hover.p-error .p-menuitem-link) {
|
||||||
|
background-color: var(--red-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮图标样式 */
|
||||||
|
:deep(.p-button-icon-only) {
|
||||||
|
width: 2.5rem !important;
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-button-icon-only .p-button-icon) {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 网络选择相关样式 */
|
||||||
|
.network-label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.network-select-container) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode adaptations */
|
||||||
|
:deep(.bg-surface-50) {
|
||||||
|
background-color: var(--surface-50, #f8fafc);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.bg-surface-0) {
|
||||||
|
background-color: var(--surface-card, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.text-primary) {
|
||||||
|
color: var(--primary-color, #3b82f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.text-secondary) {
|
||||||
|
color: var(--text-color-secondary, #64748b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:deep(.bg-surface-50) {
|
||||||
|
background-color: var(--surface-ground, #0f172a);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.bg-surface-0) {
|
||||||
|
background-color: var(--surface-card, #1e293b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design for mobile devices */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.network-header {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-content {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在小屏幕上缩短网络标签文本 */
|
||||||
|
.network-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export { default as Config } from './Config.vue';
|
export { default as Config } from './Config.vue';
|
||||||
export { default as Status } from './Status.vue';
|
export { default as Status } from './Status.vue';
|
||||||
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';
|
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';
|
||||||
|
export { default as RemoteManagement } from './RemoteManagement.vue';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
import { Config, Status, ConfigEditDialog } from "./components";
|
import { Config, Status, ConfigEditDialog, RemoteManagement } from "./components";
|
||||||
import Aura from '@primeuix/themes/aura';
|
import Aura from '@primeuix/themes/aura';
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
|
|
||||||
@@ -44,8 +44,9 @@ export default {
|
|||||||
app.component('ConfigEditDialog', ConfigEditDialog);
|
app.component('ConfigEditDialog', ConfigEditDialog);
|
||||||
app.component('Status', Status);
|
app.component('Status', Status);
|
||||||
app.component('HumanEvent', HumanEvent);
|
app.component('HumanEvent', HumanEvent);
|
||||||
|
app.component('RemoteManagement', RemoteManagement);
|
||||||
app.directive('tooltip', vTooltip as any);
|
app.directive('tooltip', vTooltip as any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils };
|
export { Config, ConfigEditDialog, RemoteManagement, Status, I18nUtils, NetworkTypes, Api, Utils };
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
|
||||||
import { Md5 } from 'ts-md5'
|
|
||||||
import { UUID } from './utils';
|
import { UUID } from './utils';
|
||||||
import { NetworkConfig } from '../types/network';
|
import { NetworkConfig } from '../types/network';
|
||||||
|
|
||||||
@@ -7,235 +5,29 @@ export interface ValidateConfigResponse {
|
|||||||
toml_config: string;
|
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 {
|
export interface ListNetworkInstanceIdResponse {
|
||||||
running_inst_ids: Array<UUID>,
|
running_inst_ids: Array<UUID>,
|
||||||
disabled_inst_ids: Array<UUID>,
|
disabled_inst_ids: Array<UUID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenerateConfigRequest {
|
|
||||||
config: NetworkConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GenerateConfigResponse {
|
export interface GenerateConfigResponse {
|
||||||
toml_config?: string;
|
toml_config?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParseConfigRequest {
|
|
||||||
toml_config: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParseConfigResponse {
|
export interface ParseConfigResponse {
|
||||||
config?: NetworkConfig;
|
config?: NetworkConfig;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiClient {
|
export interface RemoteClient {
|
||||||
private client: AxiosInstance;
|
validate_config(config: any): Promise<ValidateConfigResponse>;
|
||||||
private authFailedCb: Function | undefined;
|
run_network(config: any): Promise<undefined>;
|
||||||
|
get_network_info(inst_id: string): Promise<any>;
|
||||||
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
list_network_instance_ids(): Promise<ListNetworkInstanceIdResponse>;
|
||||||
this.client = axios.create({
|
delete_network(inst_id: string): Promise<undefined>;
|
||||||
baseURL: baseUrl + '/api/v1',
|
update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined>;
|
||||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
get_network_config(inst_id: string): Promise<any>;
|
||||||
headers: {
|
generate_config(config: NetworkConfig): Promise<GenerateConfigResponse>;
|
||||||
'Content-Type': 'application/json',
|
parse_config(toml_config: string): Promise<ParseConfigResponse>;
|
||||||
},
|
}
|
||||||
});
|
|
||||||
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 list_deivce_instance_ids(machine_id: string): Promise<ListNetworkInstanceIdResponse> {
|
|
||||||
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + machine_id + '/networks');
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update_device_instance_state(machine_id: string, inst_id: string, disabled: boolean): Promise<undefined> {
|
|
||||||
await this.client.put<string>('/machines/' + machine_id + '/networks/' + inst_id, {
|
|
||||||
disabled: disabled,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
|
|
||||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
|
|
||||||
return response.info.map;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get_network_config(machine_id: string, inst_id: string): Promise<any> {
|
|
||||||
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/config/' + inst_id);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
|
|
||||||
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
|
||||||
config: config,
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run_network(machine_id: string, config: any): Promise<undefined> {
|
|
||||||
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
|
||||||
config: config,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async delete_network(machine_id: string, inst_id: string): Promise<undefined> {
|
|
||||||
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 async generate_config(config: GenerateConfigRequest): Promise<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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
@@ -24,12 +24,13 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
// make sure to externalize deps that shouldn't be bundled
|
// make sure to externalize deps that shouldn't be bundled
|
||||||
// into your library
|
// into your library
|
||||||
external: ['vue'],
|
external: ['vue', 'primevue'],
|
||||||
output: {
|
output: {
|
||||||
// Provide global variables to use in the UMD build
|
// Provide global variables to use in the UMD build
|
||||||
// for externalized deps
|
// for externalized deps
|
||||||
globals: {
|
globals: {
|
||||||
vue: 'Vue',
|
vue: 'Vue',
|
||||||
|
primevue: 'primevue',
|
||||||
},
|
},
|
||||||
exports: "named"
|
exports: "named"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"easytier-frontend-lib": "workspace:*",
|
"easytier-frontend-lib": "workspace:*",
|
||||||
"primevue": "^4.3.9",
|
"primevue": "^4.3.9",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
|
"ts-md5": "^1.3.1",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-i18n": "^9.9.1",
|
"vue-i18n": "^9.9.1",
|
||||||
"vue-router": "4"
|
"vue-router": "4"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, inject, ref } from 'vue';
|
||||||
import { Card, Password, Button } from 'primevue';
|
import { Card, Password, Button } from 'primevue';
|
||||||
import { Api } from 'easytier-frontend-lib';
|
import ApiClient from '../modules/api';
|
||||||
|
|
||||||
const dialogRef = inject<any>('dialogRef');
|
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('');
|
const password = ref('');
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { Api } from 'easytier-frontend-lib'
|
|
||||||
import { AutoComplete, Divider, Button, Textarea } from "primevue";
|
import { AutoComplete, Divider, Button, Textarea } from "primevue";
|
||||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
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 apiHost = ref<string>(getInitialApiHost())
|
||||||
const apiHostSuggestions = ref<Array<string>>([])
|
const apiHostSuggestions = ref<Array<string>>([])
|
||||||
@@ -27,9 +27,7 @@ const errorMessage = ref<string>("");
|
|||||||
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||||
saveApiHost(apiHost.value)
|
saveApiHost(apiHost.value)
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
api.value?.generate_config({
|
api.value?.get_remote_client("").generate_config(config).then((res) => {
|
||||||
config: config
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
errorMessage.value = "Generation failed: " + res.error;
|
errorMessage.value = "Generation failed: " + res.error;
|
||||||
} else if (res.toml_config) {
|
} else if (res.toml_config) {
|
||||||
@@ -45,9 +43,7 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
|||||||
const parseConfig = async () => {
|
const parseConfig = async () => {
|
||||||
try {
|
try {
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
const res = await api.value?.parse_config({
|
const res = await api.value?.get_remote_client("").parse_config(toml_config.value);
|
||||||
toml_config: toml_config.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
errorMessage.value = "Parse failed: " + res.error;
|
errorMessage.value = "Parse failed: " + res.error;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Card, useToast } from 'primevue';
|
import { Card, useToast } from 'primevue';
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
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({
|
const props = defineProps({
|
||||||
api: Api.ApiClient,
|
api: ApiClient,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const summary = ref<Api.Summary | undefined>(undefined);
|
const summary = ref<Summary | undefined>(undefined);
|
||||||
|
|
||||||
const loadSummary = async () => {
|
const loadSummary = async () => {
|
||||||
const resp = await props.api?.get_summary();
|
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 { Button, Drawer, ProgressSpinner, useToast, InputSwitch, Popover, Dropdown, Toolbar } from 'primevue';
|
||||||
import Tooltip from 'primevue/tooltip';
|
import Tooltip from 'primevue/tooltip';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
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 DeviceDetails from './DeviceDetails.vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ApiClient from '../modules/api';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ declare const window: Window & typeof globalThis;
|
|||||||
const vTooltip = Tooltip;
|
const vTooltip = Tooltip;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
api: Api.ApiClient,
|
api: ApiClient,
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailPopover = ref();
|
const detailPopover = ref();
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { IftaLabel, Select, Button, ConfirmPopup, useConfirm, useToast, Divider, Menu } from 'primevue';
|
import { NetworkTypes, Utils, Api, RemoteManagement } from 'easytier-frontend-lib';
|
||||||
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
|
import { computed } from 'vue';
|
||||||
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n'
|
import ApiClient from '../modules/api';
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
api: Api.ApiClient;
|
api: ApiClient;
|
||||||
deviceList: Array<Utils.DeviceInfo> | undefined;
|
deviceList: Array<Utils.DeviceInfo> | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -16,7 +14,6 @@ const emits = defineEmits(['update']);
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const deviceId = computed<string>(() => {
|
const deviceId = computed<string>(() => {
|
||||||
return route.params.deviceId as 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;
|
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({
|
const selectedInstanceId = computed({
|
||||||
get() {
|
get() {
|
||||||
return instanceIdList.value.find((instance) => instance.uuid === instanceId.value);
|
return instanceId.value;
|
||||||
},
|
},
|
||||||
set(value: any) {
|
set(value: string) {
|
||||||
console.log("set instanceId", value);
|
console.log("selectedInstanceId", value);
|
||||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value.uuid } });
|
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const needShowNetworkStatus = computed(() => {
|
const remoteClient = computed<Api.RemoteClient>(() => props.api.get_remote_client(deviceId.value));
|
||||||
if (!selectedInstanceId.value) {
|
|
||||||
// nothing selected
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (networkIsDisabled.value) {
|
|
||||||
// network is disabled
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
|
|
||||||
const networkIsDisabled = computed(() => {
|
const newConfigGenerator = () => {
|
||||||
if (!selectedInstanceId.value) {
|
const config = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||||
return false;
|
config.hostname = deviceInfo.value?.hostname;
|
||||||
}
|
return config;
|
||||||
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 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="device-management">
|
<RemoteManagement :api="remoteClient" v-model:instance-id="selectedInstanceId"
|
||||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
|
:new-config-generator="newConfigGenerator" />
|
||||||
<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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { computed, onMounted, ref } from 'vue';
|
|||||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useToast } from 'primevue/usetoast';
|
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 { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ApiClient, { Credential, RegisterData } from '../modules/api';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ defineProps<{
|
|||||||
isRegistering: boolean;
|
isRegistering: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
const api = computed<ApiClient>(() => new ApiClient(apiHost.value));
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ const captchaSrc = computed(() => api.value.captcha_url());
|
|||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
// Add your login logic here
|
// Add your login logic here
|
||||||
saveApiHost(apiHost.value);
|
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);
|
let ret = await api.value?.login(credential);
|
||||||
if (ret.success) {
|
if (ret.success) {
|
||||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||||
@@ -43,8 +44,8 @@ const onSubmit = async () => {
|
|||||||
|
|
||||||
const onRegister = async () => {
|
const onRegister = async () => {
|
||||||
saveApiHost(apiHost.value);
|
saveApiHost(apiHost.value);
|
||||||
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
|
const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||||
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
|
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
|
||||||
let ret = await api.value?.register(registerReq);
|
let ret = await api.value?.register(registerReq);
|
||||||
if (ret.success) {
|
if (ret.success) {
|
||||||
toast.add({ severity: 'success', summary: 'Register Success', detail: ret.message, life: 2000 });
|
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">
|
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="register-username" class="block text-sm font-medium">{{ t('web.login.username')
|
<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" />
|
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="register-password" class="block text-sm font-medium">{{ t('web.login.password')
|
<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
|
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||||
:feedback="false" class="w-full" />
|
:feedback="false" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
||||||
import { Button, TieredMenu } from 'primevue';
|
import { Button, TieredMenu } from 'primevue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -7,13 +7,14 @@ import { useDialog } from 'primevue/usedialog';
|
|||||||
import ChangePassword from './ChangePassword.vue';
|
import ChangePassword from './ChangePassword.vue';
|
||||||
import Icon from '../assets/easytier.png'
|
import Icon from '../assets/easytier.png'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ApiClient from '../modules/api';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const api = computed<Api.ApiClient | undefined>(() => {
|
const api = computed<ApiClient | undefined>(() => {
|
||||||
try {
|
try {
|
||||||
return new Api.ApiClient(atob(route.params.apiHost as string), () => {
|
return new ApiClient(atob(route.params.apiHost as string), () => {
|
||||||
router.push({ name: 'login' });
|
router.push({ name: 'login' });
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} 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;
|
||||||
@@ -7,13 +7,19 @@ use std::sync::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use easytier::{proto::web::HeartbeatRequest, tunnel::TunnelListener};
|
use easytier::{
|
||||||
|
proto::{
|
||||||
|
api::manage::WebClientService, rpc_types::controller::BaseController, web::HeartbeatRequest,
|
||||||
|
},
|
||||||
|
rpc_service::remote_client::{self, RemoteClientManager},
|
||||||
|
tunnel::TunnelListener,
|
||||||
|
};
|
||||||
use maxminddb::geoip2;
|
use maxminddb::geoip2;
|
||||||
use session::{Location, Session};
|
use session::{Location, Session};
|
||||||
use storage::{Storage, StorageToken};
|
use storage::{Storage, StorageToken};
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
use crate::db::{Db, UserIdInDb};
|
use crate::db::{entity::user_running_network_configs, Db, UserIdInDb};
|
||||||
|
|
||||||
#[derive(rust_embed::Embed)]
|
#[derive(rust_embed::Embed)]
|
||||||
#[folder = "resources/"]
|
#[folder = "resources/"]
|
||||||
@@ -152,7 +158,7 @@ impl ClientManager {
|
|||||||
s.data().read().await.location().cloned()
|
s.data().read().await.location().cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn db(&self) -> &Db {
|
fn db(&self) -> &Db {
|
||||||
self.storage.db()
|
self.storage.db()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +251,32 @@ impl ClientManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl
|
||||||
|
RemoteClientManager<
|
||||||
|
(UserIdInDb, uuid::Uuid),
|
||||||
|
user_running_network_configs::Model,
|
||||||
|
sea_orm::DbErr,
|
||||||
|
> for ClientManager
|
||||||
|
{
|
||||||
|
fn get_rpc_client(
|
||||||
|
&self,
|
||||||
|
(user_id, machine_id): (UserIdInDb, uuid::Uuid),
|
||||||
|
) -> Option<Box<dyn WebClientService<Controller = BaseController> + Send>> {
|
||||||
|
let s = self.get_session_by_machine_id(user_id, &machine_id)?;
|
||||||
|
Some(s.scoped_rpc_client())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_storage(
|
||||||
|
&self,
|
||||||
|
) -> &impl remote_client::Storage<
|
||||||
|
(UserIdInDb, uuid::Uuid),
|
||||||
|
user_running_network_configs::Model,
|
||||||
|
sea_orm::DbErr,
|
||||||
|
> {
|
||||||
|
self.storage.db()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ use easytier::{
|
|||||||
rpc_types::{self, controller::BaseController},
|
rpc_types::{self, controller::BaseController},
|
||||||
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
|
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
|
||||||
},
|
},
|
||||||
|
rpc_service::remote_client::{ListNetworkProps, Storage as _},
|
||||||
tunnel::Tunnel,
|
tunnel::Tunnel,
|
||||||
};
|
};
|
||||||
use tokio::sync::{broadcast, RwLock};
|
use tokio::sync::{broadcast, RwLock};
|
||||||
|
|
||||||
use crate::db::ListNetworkProps;
|
|
||||||
|
|
||||||
use super::storage::{Storage, StorageToken, WeakRefStorage};
|
use super::storage::{Storage, StorageToken, WeakRefStorage};
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -224,10 +223,10 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = req.unwrap();
|
let req = req.unwrap();
|
||||||
if req.machine_id.is_none() {
|
let Some(machine_id) = req.machine_id else {
|
||||||
tracing::warn!(?req, "Machine id is not set, ignore");
|
tracing::warn!(?req, "Machine id is not set, ignore");
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
|
|
||||||
let running_inst_ids = req
|
let running_inst_ids = req
|
||||||
.running_network_instances
|
.running_network_instances
|
||||||
@@ -257,11 +256,7 @@ impl Session {
|
|||||||
|
|
||||||
let local_configs = match storage
|
let local_configs = match storage
|
||||||
.db
|
.db
|
||||||
.list_network_configs(
|
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::EnabledOnly)
|
||||||
user_id,
|
|
||||||
Some(req.machine_id.unwrap().into()),
|
|
||||||
ListNetworkProps::EnabledOnly,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(configs) => configs,
|
Ok(configs) => configs,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
|
use easytier::rpc_service::remote_client::PersistentConfig;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -39,3 +40,12 @@ impl Related<super::users::Entity> for Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
|
impl PersistentConfig for Model {
|
||||||
|
fn get_network_inst_id(&self) -> &str {
|
||||||
|
&self.network_instance_id
|
||||||
|
}
|
||||||
|
fn get_network_config(&self) -> &str {
|
||||||
|
&self.network_config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub mod entity;
|
pub mod entity;
|
||||||
|
|
||||||
|
use easytier::rpc_service::remote_client::{ListNetworkProps, Storage};
|
||||||
use entity::user_running_network_configs;
|
use entity::user_running_network_configs;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
||||||
@@ -9,17 +10,13 @@ use sea_orm::{
|
|||||||
};
|
};
|
||||||
use sea_orm_migration::MigratorTrait as _;
|
use sea_orm_migration::MigratorTrait as _;
|
||||||
use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
|
use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::migrator;
|
use crate::migrator;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
pub type UserIdInDb = i32;
|
pub type UserIdInDb = i32;
|
||||||
|
|
||||||
pub enum ListNetworkProps {
|
|
||||||
All,
|
|
||||||
EnabledOnly,
|
|
||||||
DisabledOnly,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Db {
|
pub struct Db {
|
||||||
db_path: String,
|
db_path: String,
|
||||||
@@ -68,12 +65,36 @@ impl Db {
|
|||||||
&self.orm_db
|
&self.orm_db
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn insert_or_update_user_network_config<T: ToString>(
|
pub async fn get_user_id<T: ToString>(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
user_name: T,
|
||||||
device_id: uuid::Uuid,
|
) -> Result<Option<UserIdInDb>, DbErr> {
|
||||||
network_inst_id: uuid::Uuid,
|
use entity::users as u;
|
||||||
network_config: T,
|
|
||||||
|
let user = u::Entity::find()
|
||||||
|
.filter(u::Column::Username.eq(user_name.to_string()))
|
||||||
|
.one(self.orm_db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user.map(|u| u.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: currently we don't have a token system, so we just use the user name as token
|
||||||
|
pub async fn get_user_id_by_token<T: ToString>(
|
||||||
|
&self,
|
||||||
|
token: T,
|
||||||
|
) -> Result<Option<UserIdInDb>, DbErr> {
|
||||||
|
self.get_user_id(token).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for Db {
|
||||||
|
async fn insert_or_update_user_network_config(
|
||||||
|
&self,
|
||||||
|
(user_id, device_id): (UserIdInDb, Uuid),
|
||||||
|
network_inst_id: Uuid,
|
||||||
|
network_config: impl ToString + Send,
|
||||||
) -> Result<(), DbErr> {
|
) -> Result<(), DbErr> {
|
||||||
let txn = self.orm_db().begin().await?;
|
let txn = self.orm_db().begin().await?;
|
||||||
|
|
||||||
@@ -105,10 +126,10 @@ impl Db {
|
|||||||
txn.commit().await
|
txn.commit().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_network_config(
|
async fn delete_network_config(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
(user_id, _): (UserIdInDb, Uuid),
|
||||||
network_inst_id: uuid::Uuid,
|
network_inst_id: Uuid,
|
||||||
) -> Result<(), DbErr> {
|
) -> Result<(), DbErr> {
|
||||||
use entity::user_running_network_configs as urnc;
|
use entity::user_running_network_configs as urnc;
|
||||||
|
|
||||||
@@ -121,12 +142,12 @@ impl Db {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_network_config_state(
|
async fn update_network_config_state(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
(user_id, _): (UserIdInDb, Uuid),
|
||||||
network_inst_id: uuid::Uuid,
|
network_inst_id: Uuid,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
) -> Result<entity::user_running_network_configs::Model, DbErr> {
|
) -> Result<user_running_network_configs::Model, DbErr> {
|
||||||
use entity::user_running_network_configs as urnc;
|
use entity::user_running_network_configs as urnc;
|
||||||
|
|
||||||
urnc::Entity::update_many()
|
urnc::Entity::update_many()
|
||||||
@@ -151,10 +172,9 @@ impl Db {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_network_configs(
|
async fn list_network_configs(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
(user_id, device_id): (UserIdInDb, Uuid),
|
||||||
device_id: Option<uuid::Uuid>,
|
|
||||||
props: ListNetworkProps,
|
props: ListNetworkProps,
|
||||||
) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
|
) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
|
||||||
use entity::user_running_network_configs as urnc;
|
use entity::user_running_network_configs as urnc;
|
||||||
@@ -169,7 +189,7 @@ impl Db {
|
|||||||
} else {
|
} else {
|
||||||
configs
|
configs
|
||||||
};
|
};
|
||||||
let configs = if let Some(device_id) = device_id {
|
let configs = if !device_id.is_nil() {
|
||||||
configs.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
configs.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
||||||
} else {
|
} else {
|
||||||
configs
|
configs
|
||||||
@@ -180,11 +200,10 @@ impl Db {
|
|||||||
Ok(configs)
|
Ok(configs)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_network_config(
|
async fn get_network_config(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
(user_id, device_id): (UserIdInDb, Uuid),
|
||||||
device_id: &uuid::Uuid,
|
network_inst_id: &str,
|
||||||
network_inst_id: &String,
|
|
||||||
) -> Result<Option<user_running_network_configs::Model>, DbErr> {
|
) -> Result<Option<user_running_network_configs::Model>, DbErr> {
|
||||||
use entity::user_running_network_configs as urnc;
|
use entity::user_running_network_configs as urnc;
|
||||||
|
|
||||||
@@ -197,32 +216,11 @@ impl Db {
|
|||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_id<T: ToString>(
|
|
||||||
&self,
|
|
||||||
user_name: T,
|
|
||||||
) -> Result<Option<UserIdInDb>, DbErr> {
|
|
||||||
use entity::users as u;
|
|
||||||
|
|
||||||
let user = u::Entity::find()
|
|
||||||
.filter(u::Column::Username.eq(user_name.to_string()))
|
|
||||||
.one(self.orm_db())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(user.map(|u| u.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: currently we don't have a token system, so we just use the user name as token
|
|
||||||
pub async fn get_user_id_by_token<T: ToString>(
|
|
||||||
&self,
|
|
||||||
token: T,
|
|
||||||
) -> Result<Option<UserIdInDb>, DbErr> {
|
|
||||||
self.get_user_id(token).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use easytier::rpc_service::remote_client::Storage;
|
||||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
||||||
|
|
||||||
use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
|
use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
|
||||||
@@ -235,7 +233,7 @@ mod tests {
|
|||||||
let inst_id = uuid::Uuid::new_v4();
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
let device_id = uuid::Uuid::new_v4();
|
let device_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
|
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -250,7 +248,7 @@ mod tests {
|
|||||||
|
|
||||||
// overwrite the config
|
// overwrite the config
|
||||||
let network_config = "test_config2";
|
let network_config = "test_config2";
|
||||||
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
|
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -267,14 +265,16 @@ mod tests {
|
|||||||
assert_ne!(result.update_time, result2.update_time);
|
assert_ne!(result.update_time, result2.update_time);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
db.list_network_configs(user_id, Some(device_id), ListNetworkProps::All)
|
db.list_network_configs((user_id, device_id), ListNetworkProps::All)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.len(),
|
.len(),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
db.delete_network_config(user_id, inst_id).await.unwrap();
|
db.delete_network_config((user_id, device_id), inst_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let result3 = user_running_network_configs::Entity::find()
|
let result3 = user_running_network_configs::Entity::find()
|
||||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||||
.one(db.orm_db())
|
.one(db.orm_db())
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ pub struct RestfulServer {
|
|||||||
|
|
||||||
// serve_task: Option<ScopedTask<()>>,
|
// serve_task: Option<ScopedTask<()>>,
|
||||||
// delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
// delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
||||||
network_api: NetworkApi,
|
// network_api: NetworkApi<WebClientManager>,
|
||||||
|
|
||||||
web_router: Option<Router>,
|
web_router: Option<Router>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +107,7 @@ impl RestfulServer {
|
|||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
assert!(client_mgr.is_running());
|
assert!(client_mgr.is_running());
|
||||||
|
|
||||||
let network_api = NetworkApi::new();
|
// let network_api = NetworkApi::new();
|
||||||
|
|
||||||
Ok(RestfulServer {
|
Ok(RestfulServer {
|
||||||
bind_addr,
|
bind_addr,
|
||||||
@@ -116,7 +115,7 @@ impl RestfulServer {
|
|||||||
db,
|
db,
|
||||||
// serve_task: None,
|
// serve_task: None,
|
||||||
// delete_task: None,
|
// delete_task: None,
|
||||||
network_api,
|
// network_api,
|
||||||
web_router,
|
web_router,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -188,6 +187,7 @@ impl RestfulServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused_mut)]
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
mut self,
|
mut self,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
@@ -238,7 +238,7 @@ impl RestfulServer {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/api/v1/summary", get(Self::handle_get_summary))
|
.route("/api/v1/summary", get(Self::handle_get_summary))
|
||||||
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
|
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
|
||||||
.merge(self.network_api.build_route())
|
.merge(NetworkApi::build_route())
|
||||||
.route_layer(login_required!(Backend))
|
.route_layer(login_required!(Backend))
|
||||||
.merge(auth::router())
|
.merge(auth::router())
|
||||||
.with_state(self.client_mgr.clone())
|
.with_state(self.client_mgr.clone())
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::routing::{delete, post};
|
use axum::routing::{delete, post};
|
||||||
@@ -7,12 +5,14 @@ use axum::{extract::State, routing::get, Json, Router};
|
|||||||
use axum_login::AuthUser;
|
use axum_login::AuthUser;
|
||||||
use easytier::launcher::NetworkConfig;
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::common::Void;
|
use easytier::proto::common::Void;
|
||||||
use easytier::proto::rpc_types::controller::BaseController;
|
use easytier::proto::{api::manage::*, web::*};
|
||||||
use easytier::proto::{self, api::manage::*, web::*};
|
use easytier::rpc_service::remote_client::{
|
||||||
|
ListNetworkInstanceIdsJsonResp, RemoteClientError, RemoteClientManager,
|
||||||
|
};
|
||||||
|
use sea_orm::DbErr;
|
||||||
|
|
||||||
use crate::client_manager::session::{Location, Session};
|
use crate::client_manager::session::Location;
|
||||||
use crate::client_manager::ClientManager;
|
use crate::db::UserIdInDb;
|
||||||
use crate::db::{ListNetworkProps, UserIdInDb};
|
|
||||||
|
|
||||||
use super::users::AuthSession;
|
use super::users::AuthSession;
|
||||||
use super::{
|
use super::{
|
||||||
@@ -31,6 +31,21 @@ fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
|
|||||||
(status_code, Json(error))
|
(status_code, Json(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_error(e: RemoteClientError<DbErr>) -> (StatusCode, Json<Error>) {
|
||||||
|
match e {
|
||||||
|
RemoteClientError::PersistentError(e) => convert_db_error(e),
|
||||||
|
RemoteClientError::RpcError(e) => convert_rpc_error(e),
|
||||||
|
RemoteClientError::ClientNotFound => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
other_error("Client not found").into(),
|
||||||
|
),
|
||||||
|
RemoteClientError::NotFound(msg) => (StatusCode::NOT_FOUND, other_error(msg).into()),
|
||||||
|
RemoteClientError::Other(msg) => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, other_error(msg).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
struct ValidateConfigJsonReq {
|
struct ValidateConfigJsonReq {
|
||||||
config: NetworkConfig,
|
config: NetworkConfig,
|
||||||
@@ -42,7 +57,7 @@ struct RunNetworkJsonReq {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
struct ColletNetworkInfoJsonReq {
|
struct CollectNetworkInfoJsonReq {
|
||||||
inst_ids: Option<Vec<uuid::Uuid>>,
|
inst_ids: Option<Vec<uuid::Uuid>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,12 +71,6 @@ struct RemoveNetworkJsonReq {
|
|||||||
inst_ids: Vec<uuid::Uuid>,
|
inst_ids: Vec<uuid::Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
||||||
struct ListNetworkInstanceIdsJsonResp {
|
|
||||||
running_inst_ids: Vec<easytier::proto::common::Uuid>,
|
|
||||||
disabled_inst_ids: Vec<easytier::proto::common::Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
struct ListMachineItem {
|
struct ListMachineItem {
|
||||||
client_url: Option<url::Url>,
|
client_url: Option<url::Url>,
|
||||||
@@ -74,13 +83,9 @@ struct ListMachineJsonResp {
|
|||||||
machines: Vec<ListMachineItem>,
|
machines: Vec<ListMachineItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NetworkApi {}
|
pub struct NetworkApi;
|
||||||
|
|
||||||
impl NetworkApi {
|
impl NetworkApi {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_user_id(auth_session: &AuthSession) -> Result<UserIdInDb, (StatusCode, Json<Error>)> {
|
fn get_user_id(auth_session: &AuthSession) -> Result<UserIdInDb, (StatusCode, Json<Error>)> {
|
||||||
let Some(user_id) = auth_session.user.as_ref().map(|x| x.id()) else {
|
let Some(user_id) = auth_session.user.as_ref().map(|x| x.id()) else {
|
||||||
return Err((
|
return Err((
|
||||||
@@ -91,63 +96,20 @@ impl NetworkApi {
|
|||||||
Ok(user_id)
|
Ok(user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_session_by_machine_id(
|
|
||||||
auth_session: &AuthSession,
|
|
||||||
client_mgr: &ClientManager,
|
|
||||||
machine_id: &uuid::Uuid,
|
|
||||||
) -> Result<Arc<Session>, HttpHandleError> {
|
|
||||||
let user_id = Self::get_user_id(auth_session)?;
|
|
||||||
|
|
||||||
let Some(result) = client_mgr.get_session_by_machine_id(user_id, machine_id) else {
|
|
||||||
return Err((
|
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
other_error(format!("No such session: {}", machine_id)).into(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(token) = result.get_token().await else {
|
|
||||||
return Err((
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
other_error("No token reported".to_string()).into(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
if !auth_session
|
|
||||||
.user
|
|
||||||
.as_ref()
|
|
||||||
.map(|x| x.tokens.contains(&token.token))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
return Err((
|
|
||||||
StatusCode::FORBIDDEN,
|
|
||||||
other_error("Token mismatch".to_string()).into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_validate_config(
|
async fn handle_validate_config(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path(machine_id): Path<uuid::Uuid>,
|
||||||
Json(payload): Json<ValidateConfigJsonReq>,
|
Json(payload): Json<ValidateConfigJsonReq>,
|
||||||
) -> Result<Json<ValidateConfigResponse>, HttpHandleError> {
|
) -> Result<Json<ValidateConfigResponse>, HttpHandleError> {
|
||||||
let config = payload.config;
|
Ok(client_mgr
|
||||||
let result =
|
.handle_validate_config(
|
||||||
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
|
(Self::get_user_id(&auth_session)?, machine_id),
|
||||||
|
payload.config,
|
||||||
let c = result.scoped_rpc_client();
|
|
||||||
let ret = c
|
|
||||||
.validate_config(
|
|
||||||
BaseController::default(),
|
|
||||||
ValidateConfigRequest {
|
|
||||||
config: Some(config),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_rpc_error)?;
|
.map_err(convert_error)?
|
||||||
Ok(ret.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_run_network_instance(
|
async fn handle_run_network_instance(
|
||||||
@@ -156,33 +118,13 @@ impl NetworkApi {
|
|||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path(machine_id): Path<uuid::Uuid>,
|
||||||
Json(payload): Json<RunNetworkJsonReq>,
|
Json(payload): Json<RunNetworkJsonReq>,
|
||||||
) -> Result<Json<Void>, HttpHandleError> {
|
) -> Result<Json<Void>, HttpHandleError> {
|
||||||
let config = payload.config;
|
|
||||||
let result =
|
|
||||||
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
|
|
||||||
|
|
||||||
let c = result.scoped_rpc_client();
|
|
||||||
let resp = c
|
|
||||||
.run_network_instance(
|
|
||||||
BaseController::default(),
|
|
||||||
RunNetworkInstanceRequest {
|
|
||||||
inst_id: None,
|
|
||||||
config: Some(config.clone()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(convert_rpc_error)?;
|
|
||||||
|
|
||||||
client_mgr
|
client_mgr
|
||||||
.db()
|
.handle_run_network_instance(
|
||||||
.insert_or_update_user_network_config(
|
(Self::get_user_id(&auth_session)?, machine_id),
|
||||||
auth_session.user.as_ref().unwrap().id(),
|
payload.config,
|
||||||
machine_id,
|
|
||||||
resp.inst_id.unwrap_or_default().into(),
|
|
||||||
serde_json::to_string(&config).unwrap(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_db_error)?;
|
.map_err(convert_error)?;
|
||||||
|
|
||||||
Ok(Void::default().into())
|
Ok(Void::default().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,47 +133,30 @@ impl NetworkApi {
|
|||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||||
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
|
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
|
||||||
let result =
|
Ok(client_mgr
|
||||||
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
|
.handle_collect_network_info(
|
||||||
|
(Self::get_user_id(&auth_session)?, machine_id),
|
||||||
let c = result.scoped_rpc_client();
|
Some(vec![inst_id]),
|
||||||
let ret = c
|
|
||||||
.collect_network_info(
|
|
||||||
BaseController::default(),
|
|
||||||
CollectNetworkInfoRequest {
|
|
||||||
inst_ids: vec![inst_id.into()],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_rpc_error)?;
|
.map_err(convert_error)?
|
||||||
Ok(ret.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_collect_network_info(
|
async fn handle_collect_network_info(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path(machine_id): Path<uuid::Uuid>,
|
||||||
Json(payload): Json<ColletNetworkInfoJsonReq>,
|
Json(payload): Json<CollectNetworkInfoJsonReq>,
|
||||||
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
|
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
|
||||||
let result =
|
Ok(client_mgr
|
||||||
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
|
.handle_collect_network_info(
|
||||||
|
(Self::get_user_id(&auth_session)?, machine_id),
|
||||||
let c = result.scoped_rpc_client();
|
payload.inst_ids,
|
||||||
let ret = c
|
|
||||||
.collect_network_info(
|
|
||||||
BaseController::default(),
|
|
||||||
CollectNetworkInfoRequest {
|
|
||||||
inst_ids: payload
|
|
||||||
.inst_ids
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(Into::into)
|
|
||||||
.collect(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_rpc_error)?;
|
.map_err(convert_error)?
|
||||||
Ok(ret.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_list_network_instance_ids(
|
async fn handle_list_network_instance_ids(
|
||||||
@@ -239,36 +164,11 @@ impl NetworkApi {
|
|||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path(machine_id): Path<uuid::Uuid>,
|
Path(machine_id): Path<uuid::Uuid>,
|
||||||
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
|
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
|
||||||
let result =
|
Ok(client_mgr
|
||||||
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
|
.handle_list_network_instance_ids((Self::get_user_id(&auth_session)?, machine_id))
|
||||||
|
|
||||||
let c = result.scoped_rpc_client();
|
|
||||||
let ret = c
|
|
||||||
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
|
|
||||||
.await
|
.await
|
||||||
.map_err(convert_rpc_error)?;
|
.map_err(convert_error)?
|
||||||
|
.into())
|
||||||
let running_inst_ids = ret.inst_ids.clone().into_iter().collect();
|
|
||||||
|
|
||||||
// collect networks that are disabled
|
|
||||||
let disabled_inst_ids = client_mgr
|
|
||||||
.db()
|
|
||||||
.list_network_configs(
|
|
||||||
auth_session.user.unwrap().id(),
|
|
||||||
Some(machine_id),
|
|
||||||
ListNetworkProps::DisabledOnly,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(convert_db_error)?
|
|
||||||
.iter()
|
|
||||||
.map(|x| Into::<proto::common::Uuid>::into(x.network_instance_id.clone()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(ListNetworkInstanceIdsJsonResp {
|
|
||||||
running_inst_ids,
|
|
||||||
disabled_inst_ids,
|
|
||||||
}
|
|
||||||
.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_remove_network_instance(
|
async fn handle_remove_network_instance(
|
||||||
@@ -276,25 +176,13 @@ impl NetworkApi {
|
|||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||||
) -> Result<(), HttpHandleError> {
|
) -> Result<(), HttpHandleError> {
|
||||||
let result =
|
|
||||||
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
|
|
||||||
|
|
||||||
client_mgr
|
client_mgr
|
||||||
.db()
|
.handle_remove_network_instance(
|
||||||
.delete_network_config(auth_session.user.as_ref().unwrap().id(), inst_id)
|
(Self::get_user_id(&auth_session)?, machine_id),
|
||||||
|
inst_id,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_db_error)?;
|
.map_err(convert_error)
|
||||||
|
|
||||||
let c = result.scoped_rpc_client();
|
|
||||||
c.delete_network_instance(
|
|
||||||
BaseController::default(),
|
|
||||||
DeleteNetworkInstanceRequest {
|
|
||||||
inst_ids: vec![inst_id.into()],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(convert_rpc_error)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_list_machines(
|
async fn handle_list_machines(
|
||||||
@@ -334,37 +222,14 @@ impl NetworkApi {
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let sess = Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
|
client_mgr
|
||||||
let cfg = client_mgr
|
.handle_update_network_state(
|
||||||
.db()
|
(auth_session.user.unwrap().id(), machine_id),
|
||||||
.update_network_config_state(auth_session.user.unwrap().id(), inst_id, payload.disabled)
|
inst_id,
|
||||||
.await
|
payload.disabled,
|
||||||
.map_err(convert_db_error)?;
|
|
||||||
|
|
||||||
let c = sess.scoped_rpc_client();
|
|
||||||
|
|
||||||
if payload.disabled {
|
|
||||||
c.delete_network_instance(
|
|
||||||
BaseController::default(),
|
|
||||||
DeleteNetworkInstanceRequest {
|
|
||||||
inst_ids: vec![inst_id.into()],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_rpc_error)?;
|
.map_err(convert_error)
|
||||||
} else {
|
|
||||||
c.run_network_instance(
|
|
||||||
BaseController::default(),
|
|
||||||
RunNetworkInstanceRequest {
|
|
||||||
inst_id: Some(inst_id.into()),
|
|
||||||
config: Some(serde_json::from_str(&cfg.network_config).unwrap()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(convert_rpc_error)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_network_config(
|
async fn handle_get_network_config(
|
||||||
@@ -372,31 +237,14 @@ impl NetworkApi {
|
|||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||||
) -> Result<Json<NetworkConfig>, HttpHandleError> {
|
) -> Result<Json<NetworkConfig>, HttpHandleError> {
|
||||||
let inst_id = inst_id.to_string();
|
Ok(client_mgr
|
||||||
|
.handle_get_network_config((auth_session.user.unwrap().id(), machine_id), inst_id)
|
||||||
let db_row = client_mgr
|
|
||||||
.db()
|
|
||||||
.get_network_config(auth_session.user.unwrap().id(), &machine_id, &inst_id)
|
|
||||||
.await
|
.await
|
||||||
.map_err(convert_db_error)?
|
.map_err(convert_error)?
|
||||||
.ok_or((
|
.into())
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
other_error(format!("No such network instance: {}", inst_id)).into(),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(
|
|
||||||
serde_json::from_str::<NetworkConfig>(&db_row.network_config)
|
|
||||||
.map_err(|e| {
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
other_error(format!("Failed to parse network config: {:?}", e)).into(),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_route(&mut self) -> Router<AppStateInner> {
|
pub fn build_route() -> Router<AppStateInner> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/machines", get(Self::handle_list_machines))
|
.route("/api/v1/machines", get(Self::handle_list_machines))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod vpn_portal;
|
|||||||
|
|
||||||
pub mod instance_manage;
|
pub mod instance_manage;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
pub mod remote_client;
|
||||||
|
|
||||||
pub type ApiRpcServer = self::api::ApiRpcServer;
|
pub type ApiRpcServer = self::api::ApiRpcServer;
|
||||||
|
|
||||||
|
|||||||
273
easytier/src/rpc_service/remote_client.rs
Normal file
273
easytier/src/rpc_service/remote_client.rs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::proto::{api::manage::*, rpc_types::controller::BaseController};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RemoteClientManager<T, C, E>
|
||||||
|
where
|
||||||
|
T: Copy + Send + 'static,
|
||||||
|
C: PersistentConfig + Send + 'static,
|
||||||
|
E: Send + 'static,
|
||||||
|
{
|
||||||
|
fn get_rpc_client(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
) -> Option<Box<dyn WebClientService<Controller = BaseController> + Send>>;
|
||||||
|
|
||||||
|
fn get_storage(&self) -> &impl Storage<T, C, E>;
|
||||||
|
|
||||||
|
async fn handle_validate_config(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
config: NetworkConfig,
|
||||||
|
) -> Result<ValidateConfigResponse, RemoteClientError<E>> {
|
||||||
|
let client = self
|
||||||
|
.get_rpc_client(identify)
|
||||||
|
.ok_or(RemoteClientError::ClientNotFound)?;
|
||||||
|
client
|
||||||
|
.validate_config(
|
||||||
|
BaseController::default(),
|
||||||
|
ValidateConfigRequest {
|
||||||
|
config: Some(config),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(RemoteClientError::RpcError)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_run_network_instance(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
config: NetworkConfig,
|
||||||
|
) -> Result<(), RemoteClientError<E>> {
|
||||||
|
let client = self
|
||||||
|
.get_rpc_client(identify)
|
||||||
|
.ok_or(RemoteClientError::ClientNotFound)?;
|
||||||
|
let network_config_json = serde_json::to_string(&config).map_err(|e| {
|
||||||
|
RemoteClientError::Other(format!("Failed to serialize config: {:?}", e))
|
||||||
|
})?;
|
||||||
|
let resp = client
|
||||||
|
.run_network_instance(
|
||||||
|
BaseController::default(),
|
||||||
|
RunNetworkInstanceRequest {
|
||||||
|
inst_id: None,
|
||||||
|
config: Some(config),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.get_storage()
|
||||||
|
.insert_or_update_user_network_config(
|
||||||
|
identify,
|
||||||
|
resp.inst_id.unwrap_or_default().into(),
|
||||||
|
network_config_json,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(RemoteClientError::PersistentError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_collect_network_info(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
inst_ids: Option<Vec<uuid::Uuid>>,
|
||||||
|
) -> Result<CollectNetworkInfoResponse, RemoteClientError<E>> {
|
||||||
|
let client = self
|
||||||
|
.get_rpc_client(identify)
|
||||||
|
.ok_or(RemoteClientError::ClientNotFound)?;
|
||||||
|
let resp = client
|
||||||
|
.collect_network_info(
|
||||||
|
BaseController::default(),
|
||||||
|
CollectNetworkInfoRequest {
|
||||||
|
inst_ids: inst_ids
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| id.into())
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_list_network_instance_ids(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
) -> Result<ListNetworkInstanceIdsJsonResp, RemoteClientError<E>> {
|
||||||
|
let client = self
|
||||||
|
.get_rpc_client(identify)
|
||||||
|
.ok_or(RemoteClientError::ClientNotFound)?;
|
||||||
|
let ret = client
|
||||||
|
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let running_inst_ids = ret.inst_ids.clone().into_iter().collect();
|
||||||
|
|
||||||
|
// collect networks that are disabled
|
||||||
|
let disabled_inst_ids = self
|
||||||
|
.get_storage()
|
||||||
|
.list_network_configs(identify, ListNetworkProps::DisabledOnly)
|
||||||
|
.await
|
||||||
|
.map_err(RemoteClientError::PersistentError)?
|
||||||
|
.iter()
|
||||||
|
.map(|x| Into::<crate::proto::common::Uuid>::into(x.get_network_inst_id().to_string()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(ListNetworkInstanceIdsJsonResp {
|
||||||
|
running_inst_ids,
|
||||||
|
disabled_inst_ids,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_remove_network_instance(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
inst_id: uuid::Uuid,
|
||||||
|
) -> Result<(), RemoteClientError<E>> {
|
||||||
|
let client = self
|
||||||
|
.get_rpc_client(identify)
|
||||||
|
.ok_or(RemoteClientError::ClientNotFound)?;
|
||||||
|
self.get_storage()
|
||||||
|
.delete_network_config(identify, inst_id)
|
||||||
|
.await
|
||||||
|
.map_err(RemoteClientError::PersistentError)?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.delete_network_instance(
|
||||||
|
BaseController::default(),
|
||||||
|
DeleteNetworkInstanceRequest {
|
||||||
|
inst_ids: vec![inst_id.into()],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_update_network_state(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
inst_id: uuid::Uuid,
|
||||||
|
disabled: bool,
|
||||||
|
) -> Result<(), RemoteClientError<E>> {
|
||||||
|
let client = self
|
||||||
|
.get_rpc_client(identify)
|
||||||
|
.ok_or(RemoteClientError::ClientNotFound)?;
|
||||||
|
let cfg = self
|
||||||
|
.get_storage()
|
||||||
|
.update_network_config_state(identify, inst_id, disabled)
|
||||||
|
.await
|
||||||
|
.map_err(RemoteClientError::PersistentError)?;
|
||||||
|
|
||||||
|
if disabled {
|
||||||
|
client
|
||||||
|
.delete_network_instance(
|
||||||
|
BaseController::default(),
|
||||||
|
DeleteNetworkInstanceRequest {
|
||||||
|
inst_ids: vec![inst_id.into()],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
client
|
||||||
|
.run_network_instance(
|
||||||
|
BaseController::default(),
|
||||||
|
RunNetworkInstanceRequest {
|
||||||
|
inst_id: Some(inst_id.into()),
|
||||||
|
config: Some(serde_json::from_str(cfg.get_network_config()).map_err(
|
||||||
|
|e| {
|
||||||
|
RemoteClientError::Other(format!(
|
||||||
|
"Failed to parse network config: {:?}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)?),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_get_network_config(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
inst_id: uuid::Uuid,
|
||||||
|
) -> Result<NetworkConfig, RemoteClientError<E>> {
|
||||||
|
let inst_id = inst_id.to_string();
|
||||||
|
|
||||||
|
let db_row = self
|
||||||
|
.get_storage()
|
||||||
|
.get_network_config(identify, &inst_id)
|
||||||
|
.await
|
||||||
|
.map_err(RemoteClientError::PersistentError)?
|
||||||
|
.ok_or(RemoteClientError::NotFound(format!(
|
||||||
|
"No such network instance: {}",
|
||||||
|
inst_id
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
serde_json::from_str::<NetworkConfig>(db_row.get_network_config()).map_err(|e| {
|
||||||
|
RemoteClientError::Other(format!("Failed to parse network config: {:?}", e))
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum RemoteClientError<E> {
|
||||||
|
ClientNotFound,
|
||||||
|
NotFound(String),
|
||||||
|
#[error(transparent)]
|
||||||
|
RpcError(#[from] crate::proto::rpc_types::error::Error),
|
||||||
|
PersistentError(E),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ListNetworkProps {
|
||||||
|
All,
|
||||||
|
EnabledOnly,
|
||||||
|
DisabledOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ListNetworkInstanceIdsJsonResp {
|
||||||
|
running_inst_ids: Vec<crate::proto::common::Uuid>,
|
||||||
|
disabled_inst_ids: Vec<crate::proto::common::Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PersistentConfig {
|
||||||
|
fn get_network_inst_id(&self) -> &str;
|
||||||
|
fn get_network_config(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Storage<T, C, E>: Send + Sync
|
||||||
|
where
|
||||||
|
C: PersistentConfig,
|
||||||
|
{
|
||||||
|
async fn insert_or_update_user_network_config(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
network_inst_id: Uuid,
|
||||||
|
network_config: impl ToString + Send,
|
||||||
|
) -> Result<(), E>;
|
||||||
|
|
||||||
|
async fn delete_network_config(&self, identify: T, network_inst_id: Uuid) -> Result<(), E>;
|
||||||
|
|
||||||
|
async fn update_network_config_state(
|
||||||
|
&self,
|
||||||
|
identify: T,
|
||||||
|
network_inst_id: Uuid,
|
||||||
|
disabled: bool,
|
||||||
|
) -> Result<C, E>;
|
||||||
|
|
||||||
|
async fn list_network_configs(&self, identify: T, props: ListNetworkProps)
|
||||||
|
-> Result<Vec<C>, E>;
|
||||||
|
|
||||||
|
async fn get_network_config(&self, identify: T, network_inst_id: &str) -> Result<Option<C>, E>;
|
||||||
|
}
|
||||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -159,6 +159,9 @@ importers:
|
|||||||
tailwindcss-primeui:
|
tailwindcss-primeui:
|
||||||
specifier: ^0.3.4
|
specifier: ^0.3.4
|
||||||
version: 0.3.4(tailwindcss@3.4.17)
|
version: 0.3.4(tailwindcss@3.4.17)
|
||||||
|
ts-md5:
|
||||||
|
specifier: ^1.3.1
|
||||||
|
version: 1.3.1
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.12
|
specifier: ^3.5.12
|
||||||
version: 3.5.21(typescript@5.6.3)
|
version: 3.5.21(typescript@5.6.3)
|
||||||
@@ -1086,21 +1089,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-mMB1AvqzTH25rbUo1eRfvFzNqBopX6aRlDmO1fIVVzIWi6YJNKckxbkGaatez4hH/n86IR6aEdZFM3qBUjn3Tg==}
|
resolution: {integrity: sha512-mMB1AvqzTH25rbUo1eRfvFzNqBopX6aRlDmO1fIVVzIWi6YJNKckxbkGaatez4hH/n86IR6aEdZFM3qBUjn3Tg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@oxc-resolver/binding-linux-arm64-musl@4.2.0':
|
'@oxc-resolver/binding-linux-arm64-musl@4.2.0':
|
||||||
resolution: {integrity: sha512-9oPBU8Yb35z15/14LzALn/8rRwwrtfe19l25N1MRZVSONGiOwfzWNqDNjWiDdyW+EUt/hlylmFOItZmreL6iIw==}
|
resolution: {integrity: sha512-9oPBU8Yb35z15/14LzALn/8rRwwrtfe19l25N1MRZVSONGiOwfzWNqDNjWiDdyW+EUt/hlylmFOItZmreL6iIw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@oxc-resolver/binding-linux-x64-gnu@4.2.0':
|
'@oxc-resolver/binding-linux-x64-gnu@4.2.0':
|
||||||
resolution: {integrity: sha512-8wU4fwHb0b45i0qMBJ24UYBEtaLyvYWUOqVVCn0SpQZ1mhWWC8dvD6+zIVAKRVex/cKdgzi3imXoKGIDqVEu9w==}
|
resolution: {integrity: sha512-8wU4fwHb0b45i0qMBJ24UYBEtaLyvYWUOqVVCn0SpQZ1mhWWC8dvD6+zIVAKRVex/cKdgzi3imXoKGIDqVEu9w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@oxc-resolver/binding-linux-x64-musl@4.2.0':
|
'@oxc-resolver/binding-linux-x64-musl@4.2.0':
|
||||||
resolution: {integrity: sha512-5CS2wlGxzESPJCj4NlNGr73QCku75VpGtkwNp8qJF4hLELKAzkoqIB0eBbcvNPg8m2rB7YeXb1u+puGUKXDhNQ==}
|
resolution: {integrity: sha512-5CS2wlGxzESPJCj4NlNGr73QCku75VpGtkwNp8qJF4hLELKAzkoqIB0eBbcvNPg8m2rB7YeXb1u+puGUKXDhNQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@oxc-resolver/binding-wasm32-wasi@4.2.0':
|
'@oxc-resolver/binding-wasm32-wasi@4.2.0':
|
||||||
resolution: {integrity: sha512-VOLpvmVAQZjvj/7Et/gYzW6yBqL9VKjLWOGaFiQ7cvTpY9R9d/1mrNKEuP3beDHF2si2fM5f2pl9bL+N4tvwiA==}
|
resolution: {integrity: sha512-VOLpvmVAQZjvj/7Et/gYzW6yBqL9VKjLWOGaFiQ7cvTpY9R9d/1mrNKEuP3beDHF2si2fM5f2pl9bL+N4tvwiA==}
|
||||||
@@ -1232,56 +1239,67 @@ packages:
|
|||||||
resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
|
resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.50.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.50.1':
|
||||||
resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
|
resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.50.1':
|
'@rollup/rollup-linux-arm64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
|
resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.50.1':
|
'@rollup/rollup-linux-arm64-musl@4.50.1':
|
||||||
resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
|
resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loongarch64-gnu@4.50.1':
|
'@rollup/rollup-linux-loongarch64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
|
resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.50.1':
|
'@rollup/rollup-linux-ppc64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
|
resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.50.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
|
resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.50.1':
|
'@rollup/rollup-linux-riscv64-musl@4.50.1':
|
||||||
resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
|
resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.50.1':
|
'@rollup/rollup-linux-s390x-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
|
resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.50.1':
|
'@rollup/rollup-linux-x64-gnu@4.50.1':
|
||||||
resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
|
resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.50.1':
|
'@rollup/rollup-linux-x64-musl@4.50.1':
|
||||||
resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
|
resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-openharmony-arm64@4.50.1':
|
'@rollup/rollup-openharmony-arm64@4.50.1':
|
||||||
resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
|
resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
|
||||||
@@ -1368,30 +1386,35 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-arm64-musl@2.7.1':
|
'@tauri-apps/cli-linux-arm64-musl@2.7.1':
|
||||||
resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==}
|
resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-riscv64-gnu@2.7.1':
|
'@tauri-apps/cli-linux-riscv64-gnu@2.7.1':
|
||||||
resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==}
|
resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-gnu@2.7.1':
|
'@tauri-apps/cli-linux-x64-gnu@2.7.1':
|
||||||
resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==}
|
resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tauri-apps/cli-linux-x64-musl@2.7.1':
|
'@tauri-apps/cli-linux-x64-musl@2.7.1':
|
||||||
resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==}
|
resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tauri-apps/cli-win32-arm64-msvc@2.7.1':
|
'@tauri-apps/cli-win32-arm64-msvc@2.7.1':
|
||||||
resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==}
|
resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==}
|
||||||
@@ -1577,41 +1600,49 @@ packages:
|
|||||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||||
@@ -1746,6 +1777,7 @@ packages:
|
|||||||
'@vue-macros/define-props@4.0.6':
|
'@vue-macros/define-props@4.0.6':
|
||||||
resolution: {integrity: sha512-cfFg84z9/qa0HNpkubERQOcBBkLo2Y9RpI8BXq/tl4gceuR6++ycIgqZZMSxoaLdet0VnDv+CMRz3yHGVSClKw==}
|
resolution: {integrity: sha512-cfFg84z9/qa0HNpkubERQOcBBkLo2Y9RpI8BXq/tl4gceuR6++ycIgqZZMSxoaLdet0VnDv+CMRz3yHGVSClKw==}
|
||||||
engines: {node: '>=16.14.0'}
|
engines: {node: '>=16.14.0'}
|
||||||
|
deprecated: Use v3
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@vue-macros/reactivity-transform': ^1.1.6
|
'@vue-macros/reactivity-transform': ^1.1.6
|
||||||
vue: ^2.7.0 || ^3.2.25
|
vue: ^2.7.0 || ^3.2.25
|
||||||
|
|||||||
Reference in New Issue
Block a user