make all frontend functions works (#466)
This commit is contained in:
@@ -2,12 +2,7 @@
|
||||
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { onMounted } from 'vue';
|
||||
import Login from './components/Login.vue'
|
||||
import { Button } from 'primevue';
|
||||
import ApiClient from './modules/api';
|
||||
import DeviceList from './components/DeviceList.vue';
|
||||
|
||||
const api = new ApiClient('http://10.147.223.128:11211/api/v1/'); // Replace with actual API URL
|
||||
import { Toast, DynamicDialog } from 'primevue';
|
||||
|
||||
onMounted(async () => {
|
||||
await I18nUtils.loadLanguageAsync('cn')
|
||||
@@ -18,109 +13,10 @@ onMounted(async () => {
|
||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||
|
||||
<template>
|
||||
<div id="root" class="">
|
||||
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<button data-drawer-target="logo-sidebar" data-drawer-toggle="logo-sidebar" aria-controls="logo-sidebar"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600">
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<i class="pi pi-list" style="font-size: 1.3rem"></i>
|
||||
</button>
|
||||
<a href="https://flowbite.com" class="flex ms-2 md:me-24">
|
||||
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8 me-3" alt="FlowBite Logo" />
|
||||
<span
|
||||
class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">EasyTier</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center ms-3">
|
||||
<div>
|
||||
<button type="button"
|
||||
class="flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||
aria-expanded="false" data-dropdown-toggle="dropdown-user">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<img class="w-8 h-8 rounded-full" src="https://flowbite.com/docs/images/people/profile-picture-5.jpg"
|
||||
alt="user photo">
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
||||
Neil Sims
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
|
||||
neil.sims@flowbite.com
|
||||
</p>
|
||||
</div>
|
||||
<ul class="py-1" role="none">
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Dashboard</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Earnings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<Toast />
|
||||
<DynamicDialog />
|
||||
|
||||
<aside id="logo-sidebar"
|
||||
class="fixed top-0 left-0 z-40 w-64 h-screen pt-20 transition-transform -translate-x-full bg-white border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
aria-label="Sidebar">
|
||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
||||
<ul class="space-y-2 font-medium">
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5" severity="contrast">
|
||||
<i class="pi pi-chart-pie" style="font-size: 1.2rem"></i>
|
||||
<span class="mb-0.5">DashBoard</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5" severity="contrast">
|
||||
<i class="pi pi-server" style="font-size: 1.2rem"></i>
|
||||
<span class="mb-0.5">Devices</span>
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="p-4 sm:ml-64">
|
||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700 mt-14">
|
||||
<div class="grid grid-cols-1 gap-4 mb-4">
|
||||
<DeviceList :api="api"></DeviceList>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 mb-4">
|
||||
<Login :api="api"></Login>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { Card, Password, Button } from 'primevue';
|
||||
import { Api } from 'easytier-frontend-lib';
|
||||
|
||||
const dialogRef = inject<any>('dialogRef');
|
||||
|
||||
const api = computed<Api.ApiClient>(() => dialogRef.value.data.api);
|
||||
|
||||
const password = ref('');
|
||||
|
||||
const changePassword = async () => {
|
||||
await api.value.change_password(password.value);
|
||||
dialogRef.value.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold text-center">Change Password
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<Password v-model="password" placeholder="New Password" :feedback="false" toggleMask />
|
||||
<Button @click="changePassword" label="Ok" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { Card, useToast } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const summary = ref<Api.Summary | undefined>(undefined);
|
||||
|
||||
const loadSummary = async () => {
|
||||
const resp = await props.api?.get_summary();
|
||||
summary.value = resp;
|
||||
};
|
||||
|
||||
const periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await loadSummary();
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Load Summary Failed', detail: e, life: 2000 });
|
||||
console.error(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
});
|
||||
|
||||
const deviceCount = computed<number | undefined>(
|
||||
() => {
|
||||
return summary.value?.device_count;
|
||||
},
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<Card class="h-full">
|
||||
<template #title>Device Count</template>
|
||||
<template #content>
|
||||
<div class="w-full flex justify-center text-7xl font-bold text-green-800 mt-4">
|
||||
{{ deviceCount }}
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="flex items-center justify-center rounded bg-gray-50 dark:bg-gray-800">
|
||||
<p class="text-2xl text-gray-400 dark:text-gray-500">
|
||||
<!-- <svg class="w-3.5 h-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 18 18">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 1v16M1 9h16" />
|
||||
</svg> -->
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -1,204 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import ApiClient, { ValidateConfigResponse } from '../modules/api';
|
||||
import { Config, Status, NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { Button, Column, DataTable, Drawer, Toolbar, IftaLabel, Select, Dialog, ConfirmPopup, useConfirm } from 'primevue';
|
||||
|
||||
function toHexString(uint64: bigint, padding = 9): string {
|
||||
let hexString = uint64.toString(16);
|
||||
while (hexString.length < padding) {
|
||||
hexString = '0' + hexString;
|
||||
}
|
||||
return hexString;
|
||||
}
|
||||
|
||||
function uint32ToUuid(part1: number, part2: number, part3: number, part4: number): string {
|
||||
// 将两个 uint64 转换为 16 进制字符串
|
||||
const part1Hex = toHexString(BigInt(part1), 8);
|
||||
const part2Hex = toHexString(BigInt(part2), 8);
|
||||
const part3Hex = toHexString(BigInt(part3), 8);
|
||||
const part4Hex = toHexString(BigInt(part4), 8);
|
||||
|
||||
// 构造 UUID 格式字符串
|
||||
const uuid = `${part1Hex.substring(0, 8)}-${part2Hex.substring(0, 4)}-${part2Hex.substring(4, 8)}-${part3Hex.substring(0, 4)}-${part3Hex.substring(4, 8)}${part4Hex.substring(0, 12)}`;
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
interface UUID {
|
||||
part1: number;
|
||||
part2: number;
|
||||
part3: number;
|
||||
part4: number;
|
||||
}
|
||||
|
||||
function UuidToStr(uuid: UUID): string {
|
||||
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
|
||||
}
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Button, Column, DataTable, Drawer, ProgressSpinner, useToast } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
|
||||
const props = defineProps({
|
||||
api: ApiClient,
|
||||
api: Api.ApiClient,
|
||||
});
|
||||
|
||||
const api = props.api;
|
||||
|
||||
interface DeviceList {
|
||||
hostname: string;
|
||||
public_ip: string;
|
||||
running_network_count: number;
|
||||
report_time: string;
|
||||
easytier_version: string;
|
||||
running_network_instances?: Array<string>;
|
||||
machine_id: string;
|
||||
}
|
||||
const deviceList = ref<Array<Utils.DeviceInfo> | undefined>(undefined);
|
||||
|
||||
const selectedDevice = ref<DeviceList | null>(null);
|
||||
const deviceList = ref<Array<DeviceList>>([]);
|
||||
const instanceIdList = computed(() => {
|
||||
let insts = selectedDevice.value?.running_network_instances || [];
|
||||
let options = insts.map((instance: string) => {
|
||||
return { uuid: instance };
|
||||
});
|
||||
console.log("options", options);
|
||||
return options;
|
||||
});
|
||||
const selectedInstanceId = ref<any | null>(null);
|
||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
const selectedDeviceId = computed<string | undefined>(() => route.params.deviceId as string);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const loadDevices = async () => {
|
||||
const resp = await api?.list_machines();
|
||||
console.log(resp);
|
||||
let devices: Array<DeviceList> = [];
|
||||
let devices: Array<Utils.DeviceInfo> = [];
|
||||
for (const device of (resp || [])) {
|
||||
devices.push({
|
||||
hostname: device.info?.hostname,
|
||||
public_ip: device.client_url,
|
||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => UuidToStr(instance)),
|
||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)),
|
||||
running_network_count: device.info?.running_network_instances.length,
|
||||
report_time: device.info?.report_time,
|
||||
easytier_version: device.info?.easytier_version,
|
||||
machine_id: UuidToStr(device.info?.machine_id),
|
||||
machine_id: Utils.UuidToStr(device.info?.machine_id),
|
||||
});
|
||||
}
|
||||
console.debug("device list", deviceList.value);
|
||||
deviceList.value = devices;
|
||||
console.log(deviceList.value);
|
||||
};
|
||||
|
||||
interface SelectedDevice {
|
||||
machine_id: string;
|
||||
instance_id: string;
|
||||
}
|
||||
|
||||
const checkDeviceSelected = (): SelectedDevice => {
|
||||
let machine_id = selectedDevice.value?.machine_id;
|
||||
let inst_id = selectedInstanceId.value?.uuid;
|
||||
if (machine_id && inst_id) {
|
||||
return { machine_id, instance_id: inst_id };
|
||||
} else {
|
||||
throw new Error("No device selected");
|
||||
const periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await loadDevices();
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Load Device List Failed', detail: e, life: 2000 });
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const loadDeviceInfo = async () => {
|
||||
let selectedDevice = checkDeviceSelected();
|
||||
if (!selectedDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret = await api?.get_network_info(selectedDevice.machine_id, selectedDevice.instance_id);
|
||||
let device_info = ret[selectedDevice.instance_id]
|
||||
|
||||
curNetworkInfo.value = {
|
||||
instance_id: selectedDevice.instance_id,
|
||||
running: device_info.running,
|
||||
error_msg: device_info.error_msg,
|
||||
detail: device_info,
|
||||
} as NetworkTypes.NetworkInstance;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
setInterval(loadDevices, 1000);
|
||||
setInterval(loadDeviceInfo, 1000);
|
||||
periodFunc.start();
|
||||
});
|
||||
|
||||
const visibleRight = ref(false);
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
});
|
||||
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
||||
let machine_id = selectedDevice.value?.machine_id;
|
||||
if (!machine_id) {
|
||||
throw new Error("No machine selected");
|
||||
}
|
||||
|
||||
if (!newNetworkConfig.value) {
|
||||
throw new Error("No network config");
|
||||
}
|
||||
|
||||
let ret = await api?.validate_config(machine_id, newNetworkConfig.value);
|
||||
console.log("verifyNetworkConfig", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
const createNewNetwork = async () => {
|
||||
let config = await verifyNetworkConfig();
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
let machine_id = selectedDevice.value?.machine_id;
|
||||
if (!machine_id) {
|
||||
throw new Error("No machine selected");
|
||||
}
|
||||
|
||||
let ret = await api?.run_network(machine_id, config?.toml_config);
|
||||
console.log("createNewNetwork", ret);
|
||||
showCreateNetworkDialog.value = false;
|
||||
await loadDevices();
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const ret = checkDeviceSelected();
|
||||
await api?.delete_network(ret?.machine_id, ret?.instance_id);
|
||||
await loadDevices();
|
||||
},
|
||||
reject: () => {
|
||||
return;
|
||||
const deviceManageVisible = computed<boolean>({
|
||||
get: () => !!selectedDeviceId.value,
|
||||
set: (value) => {
|
||||
if (!value) {
|
||||
router.push({ name: 'deviceList', params: { deviceId: undefined } });
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const selectedDeviceHostname = computed<string | undefined>(() => {
|
||||
return deviceList.value?.find((device) => device.machine_id === selectedDeviceId.value)?.hostname;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
<template>
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
<Dialog v-model:visible="showCreateNetworkDialog" modal header="Create New Network" :style="{ width: '55rem' }">
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</Dialog>
|
||||
<div v-if="deviceList === undefined" class="w-full flex justify-center">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
|
||||
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
||||
:sortOrder="-1">
|
||||
:sortOrder="-1" v-if="deviceList !== undefined">
|
||||
<template #header>
|
||||
<div class="text-xl font-bold">Device List</div>
|
||||
</template>
|
||||
|
||||
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
||||
<Column field="public_ip" header="Public IP" style="width: 150px"></Column>
|
||||
<Column field="running_network_count" header="Running Network Count" sortable style="width: 150px"></Column>
|
||||
@@ -206,38 +88,23 @@ const confirmDeleteNetwork = (event: any) => {
|
||||
<Column field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
||||
<Column class="w-24 !text-end">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-search" @click="selectedDevice = data; visibleRight = true" severity="secondary"
|
||||
rounded></Button>
|
||||
<Button icon="pi pi-cog"
|
||||
@click="router.push({ name: 'deviceManagement', params: { deviceId: data.machine_id, instanceId: data.running_network_instances[0] } })"
|
||||
severity="secondary" rounded></Button>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-start">
|
||||
<div class="flex justify-end">
|
||||
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<Drawer v-model:visible="visibleRight" header="Device Management" position="right" class="w-1/2 min-w-96">
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<IftaLabel>
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid"
|
||||
inputId="dd-inst-id" placeholder="Select Instance" />
|
||||
<label class="mr-3" for="dd-inst-id">Network</label>
|
||||
</IftaLabel>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="gap-x-3 flex">
|
||||
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
||||
iconPos="right" />
|
||||
<Button @click="showCreateNetworkDialog = true" icon="pi pi-plus" label="Create" iconPos="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo">
|
||||
|
||||
</Status>
|
||||
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
||||
class="w-1/2 min-w-96">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
||||
</RouterView>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast } from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
api: Api.ApiClient;
|
||||
deviceList: Array<Utils.DeviceInfo> | undefined;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update']);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const deviceId = computed<string>(() => {
|
||||
return route.params.deviceId as string;
|
||||
});
|
||||
|
||||
const instanceId = computed<string>(() => {
|
||||
return route.params.instanceId as string;
|
||||
});
|
||||
|
||||
const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
|
||||
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
|
||||
});
|
||||
|
||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const isEditing = ref(false);
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const instanceIdList = computed(() => {
|
||||
let insts = deviceInfo.value?.running_network_instances || [];
|
||||
let options = 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);
|
||||
router.push({ name: 'deviceManagement', params: { deviceId: deviceId.value, instanceId: value.uuid } });
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const newNetwork = () => {
|
||||
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
isEditing.value = false;
|
||||
showCreateNetworkDialog.value = true;
|
||||
}
|
||||
|
||||
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;
|
||||
} 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 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;
|
||||
}
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await loadDeviceInfo();
|
||||
} catch (e) {
|
||||
console.debug(e);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
||||
:style="{ width: '55rem' }">
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</Dialog>
|
||||
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<IftaLabel>
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" inputId="dd-inst-id"
|
||||
placeholder="Select Instance" />
|
||||
<label class="mr-3" for="dd-inst-id">Network</label>
|
||||
</IftaLabel>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="gap-x-3 flex">
|
||||
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
||||
iconPos="right" />
|
||||
<Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" />
|
||||
<Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="!!selectedInstanceId">
|
||||
</Status>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 place-content-center h-full" v-if="!selectedInstanceId">
|
||||
<div class="text-center text-xl"> Select or create a network instance to manage </div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { Api } from 'easytier-frontend-lib';
|
||||
|
||||
defineProps<{
|
||||
isRegistering: boolean;
|
||||
}>();
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const registerUsername = ref('');
|
||||
const registerPassword = ref('');
|
||||
const captcha = ref('');
|
||||
const captchaSrc = computed(() => api.value.captcha_url());
|
||||
|
||||
const onSubmit = async () => {
|
||||
// Add your login logic here
|
||||
const credential: Api.Credential = { username: username.value, password: password.value, };
|
||||
let ret = await api.value?.login(credential);
|
||||
if (ret.success) {
|
||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||
router.push({
|
||||
name: 'dashboard',
|
||||
params: { apiHost: btoa(apiHost.value) },
|
||||
});
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'Login Failed', detail: ret.message, life: 2000 });
|
||||
}
|
||||
};
|
||||
|
||||
const onRegister = async () => {
|
||||
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
|
||||
let ret = await api.value?.register(registerReq);
|
||||
if (ret.success) {
|
||||
toast.add({ severity: 'success', summary: 'Register Success', detail: ret.message, life: 2000 });
|
||||
router.push({ name: 'login' });
|
||||
} else {
|
||||
toast.add({ severity: 'error', summary: 'Register Failed', detail: ret.message, life: 2000 });
|
||||
}
|
||||
};
|
||||
|
||||
const defaultApiHost = 'http://10.147.223.128:11211'
|
||||
const apiHost = ref<string>(defaultApiHost)
|
||||
const apiHostSuggestions = ref<Array<string>>([])
|
||||
const apiHostSearch = async (event: { query: string }) => {
|
||||
apiHostSuggestions.value = [];
|
||||
if (event.query) {
|
||||
apiHostSuggestions.value.push(event.query);
|
||||
}
|
||||
apiHostSuggestions.value.push(defaultApiHost);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
@@ -6,6 +68,11 @@
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-field mb-4">
|
||||
<label for="api-host" class="block text-sm font-medium">Api Host</label>
|
||||
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
||||
@complete="apiHostSearch" class="w-full" />
|
||||
</div>
|
||||
<form v-if="!isRegistering" @submit.prevent="onSubmit" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="username" class="block text-sm font-medium">Username</label>
|
||||
@@ -13,14 +80,14 @@
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="password" class="block text-sm font-medium">Password</label>
|
||||
<Password id="password" v-model="password" required toggleMask />
|
||||
<Password id="password" v-model="password" required toggleMask :feedback="false" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Login" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Register" type="button" class="w-full" @click="isRegistering = true"
|
||||
severity="secondary" />
|
||||
<Button label="Register" type="button" class="w-full"
|
||||
@click="$router.replace({ name: 'register' })" severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -32,7 +99,7 @@
|
||||
<div class="p-field">
|
||||
<label for="register-password" class="block text-sm font-medium">Password</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
class="w-full" />
|
||||
:feedback="false" class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
||||
@@ -43,8 +110,8 @@
|
||||
<Button label="Register" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Back to Login" type="button" class="w-full" @click="isRegistering = false"
|
||||
severity="secondary" />
|
||||
<Button label="Back to Login" type="button" class="w-full"
|
||||
@click="$router.replace({ name: 'login' })" severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -52,42 +119,4 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Card, InputText, Password, Button } from 'primevue';
|
||||
import ApiClient from '../modules/api';
|
||||
import { Credential } from '../modules/api';
|
||||
|
||||
const props = defineProps({
|
||||
api: ApiClient,
|
||||
});
|
||||
|
||||
const api = props.api;
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const registerUsername = ref('');
|
||||
const registerPassword = ref('');
|
||||
const captcha = ref('');
|
||||
const captchaSrc = computed(() => api?.captcha_url());
|
||||
const isRegistering = ref(false);
|
||||
|
||||
|
||||
const onSubmit = async () => {
|
||||
console.log('Username:', username.value);
|
||||
console.log('Password:', password.value);
|
||||
// Add your login logic here
|
||||
const credential: Credential = { username: username.value, password: password.value, };
|
||||
const ret = await api?.login(credential);
|
||||
alert(ret?.message);
|
||||
};
|
||||
|
||||
const onRegister = () => {
|
||||
console.log('Register Username:', registerUsername.value);
|
||||
console.log('Register Password:', registerPassword.value);
|
||||
console.log('Captcha:', captcha.value);
|
||||
// Add your register logic here
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib'
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { Button, TieredMenu } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useDialog } from 'primevue/usedialog';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import Icon from '../assets/easytier.png'
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const api = computed<Api.ApiClient | undefined>(() => {
|
||||
try {
|
||||
return new Api.ApiClient(atob(route.params.apiHost as string), () => {
|
||||
router.push({ name: 'login' });
|
||||
})
|
||||
} catch (e) {
|
||||
router.push({ name: 'login' });
|
||||
}
|
||||
});
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
onMounted(async () => {
|
||||
await I18nUtils.loadLanguageAsync('cn')
|
||||
});
|
||||
|
||||
const userMenu = ref();
|
||||
const userMenuItems = ref([
|
||||
{
|
||||
label: 'Change Password',
|
||||
icon: 'pi pi-key',
|
||||
command: () => {
|
||||
console.log('File');
|
||||
let ret = dialog.open(ChangePassword, {
|
||||
props: {
|
||||
modal: true,
|
||||
},
|
||||
data: {
|
||||
api: api.value,
|
||||
}
|
||||
});
|
||||
|
||||
console.log("return", ret)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
icon: 'pi pi-sign-out',
|
||||
command: async () => {
|
||||
try {
|
||||
await api.value?.logout();
|
||||
} catch (e) {
|
||||
console.error("logout failed", e);
|
||||
}
|
||||
router.push({ name: 'login' });
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const forceShowSideBar = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||
<template>
|
||||
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<div class="sm:hidden">
|
||||
<Button type="button" aria-haspopup="true" icon="pi pi-list" variant="text" size="large"
|
||||
severity="contrast" @click="forceShowSideBar = !forceShowSideBar" />
|
||||
</div>
|
||||
<a href="https://easytier.top" class="flex ms-2 md:me-24">
|
||||
<img :src="Icon" class="h-9 me-3" alt="FlowBite Logo" />
|
||||
<span
|
||||
class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">EasyTier</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center ms-3">
|
||||
<div>
|
||||
<Button type="button" @click="userMenu.toggle($event)" aria-haspopup="true"
|
||||
aria-controls="user-menu" icon="pi pi-user" raised rounded />
|
||||
<TieredMenu ref="userMenu" id="user-menu" :model="userMenuItems" popup />
|
||||
</div>
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
||||
Neil Sims
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
|
||||
neil.sims@flowbite.com
|
||||
</p>
|
||||
</div>
|
||||
<ul class="py-1" role="none">
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Dashboard</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Earnings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside id="logo-sidebar"
|
||||
class="fixed top-1 left-0 z-40 w-64 h-screen pt-20 transition-transform bg-white border-r border-gray-201 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
:class="{ '-translate-x-full': !forceShowSideBar }" aria-label="Sidebar">
|
||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
||||
<ul class="space-y-2 font-medium">
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'dashboard' })">
|
||||
<i class="pi pi-chart-pie text-xl"></i>
|
||||
<span class="mb-0.5">DashBoard</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'deviceList' })">
|
||||
<i class="pi pi-server text-xl"></i>
|
||||
<span class="mb-0.5">Devices</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'login' })">
|
||||
<i class="pi pi-sign-in text-xl"></i>
|
||||
<span class="mb-0.5">Login Page</span>
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="p-4 sm:ml-64">
|
||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700 mt-14">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :api="api" />
|
||||
</RouterView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-button {
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,72 @@ import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import MainPage from './components/MainPage.vue'
|
||||
import Login from './components/Login.vue'
|
||||
import DeviceList from './components/DeviceList.vue'
|
||||
import DeviceManagement from './components/DeviceManagement.vue'
|
||||
import Dashboard from './components/Dashboard.vue'
|
||||
import DialogService from 'primevue/dialogservice';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/auth', children: [
|
||||
{
|
||||
name: 'login',
|
||||
path: '',
|
||||
component: Login,
|
||||
alias: 'login',
|
||||
props: { isRegistering: false }
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
path: 'register',
|
||||
component: Login,
|
||||
props: { isRegistering: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/h/:apiHost', component: MainPage, children: [
|
||||
{
|
||||
path: '',
|
||||
alias: 'dashboard',
|
||||
name: 'dashboard',
|
||||
component: Dashboard,
|
||||
},
|
||||
{
|
||||
path: 'deviceList',
|
||||
name: 'deviceList',
|
||||
component: DeviceList,
|
||||
children: [
|
||||
{
|
||||
path: 'device/:deviceId/:instanceId?',
|
||||
name: 'deviceManagement',
|
||||
component: DeviceManagement,
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*', name: 'notFound', redirect: () => {
|
||||
let apiHost = localStorage.getItem('apiHost');
|
||||
if (apiHost) {
|
||||
return { name: 'dashboard', params: { apiHost: apiHost } }
|
||||
} else {
|
||||
return { name: 'login' }
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
createApp(App).use(PrimeVue,
|
||||
{
|
||||
theme: {
|
||||
@@ -21,4 +87,4 @@ createApp(App).use(PrimeVue,
|
||||
}
|
||||
}
|
||||
}
|
||||
).use(ConfirmationService as any).use(EasytierFrontendLib).mount('#app')
|
||||
).use(ToastService as any).use(DialogService as any).use(router).use(ConfirmationService as any).use(EasytierFrontendLib).mount('#app')
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
// 定义接口返回的数据结构
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
message: string;
|
||||
user: any; // 同上
|
||||
}
|
||||
|
||||
// 定义请求体数据结构
|
||||
export interface Credential {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterData {
|
||||
credential: Credential;
|
||||
captcha: string;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl,
|
||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 添加请求拦截器
|
||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
return config;
|
||||
}, (error: any) => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.log('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
// 请求已发出,但是服务器响应的状态码不在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 login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
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 register(data: RegisterData): Promise<RegisterResponse> {
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
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_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 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: string): 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 captcha_url() {
|
||||
return this.client.defaults.baseURL + 'auth/captcha';
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
Reference in New Issue
Block a user