mirror of
https://github.com/EasyTier/EasyTier.git
synced 2024-11-16 11:42:27 +08:00
make all frontend functions works (#466)
Some checks are pending
EasyTier Core / pre_job (push) Waiting to run
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Blocked by required conditions
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier Core / core-result (push) Blocked by required conditions
EasyTier GUI / pre_job (push) Waiting to run
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier GUI / gui-result (push) Blocked by required conditions
EasyTier Mobile / pre_job (push) Waiting to run
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Blocked by required conditions
EasyTier Mobile / mobile-result (push) Blocked by required conditions
EasyTier Test / pre_job (push) Waiting to run
EasyTier Test / test (push) Blocked by required conditions
Some checks are pending
EasyTier Core / pre_job (push) Waiting to run
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Blocked by required conditions
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Blocked by required conditions
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Blocked by required conditions
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier Core / core-result (push) Blocked by required conditions
EasyTier GUI / pre_job (push) Waiting to run
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Blocked by required conditions
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Blocked by required conditions
EasyTier GUI / gui-result (push) Blocked by required conditions
EasyTier Mobile / pre_job (push) Waiting to run
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Blocked by required conditions
EasyTier Mobile / mobile-result (push) Blocked by required conditions
EasyTier Test / pre_job (push) Waiting to run
EasyTier Test / test (push) Blocked by required conditions
This commit is contained in:
parent
e948dbfcc1
commit
88e6de9d7e
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2006,6 +2006,7 @@ dependencies = [
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -43,6 +43,7 @@ clap = { version = "4.4.8", features = [
|
||||||
"wrap_help",
|
"wrap_help",
|
||||||
] }
|
] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
uuid = { version = "1.5.0", features = [
|
uuid = { version = "1.5.0", features = [
|
||||||
"v4",
|
"v4",
|
||||||
"fast-rng",
|
"fast-rng",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@primevue/themes": "^4.2.1",
|
"@primevue/themes": "^4.2.1",
|
||||||
"@vueuse/core": "^11.1.0",
|
"@vueuse/core": "^11.1.0",
|
||||||
"aura": "link:@primevue\\themes\\aura",
|
"aura": "link:@primevue\\themes\\aura",
|
||||||
|
"axios": "^1.7.7",
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.2.1",
|
"primevue": "^4.2.1",
|
||||||
|
|
35
easytier-web/frontend-lib/src/components/HumanEvent.vue
Normal file
35
easytier-web/frontend-lib/src/components/HumanEvent.vue
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { EventType } from '../types/network'
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Fieldset } from 'primevue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
event: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const eventKey = computed(() => {
|
||||||
|
const key = Object.keys(props.event)[0]
|
||||||
|
return Object.keys(EventType).includes(key) ? key : 'Unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
const eventValue = computed(() => {
|
||||||
|
const value = props.event[eventKey.value]
|
||||||
|
return typeof value === 'object' ? value : value
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Fieldset :legend="t(`event.${eventKey}`)">
|
||||||
|
<template v-if="eventKey !== 'Unknown'">
|
||||||
|
<div v-if="event.DhcpIpv4Changed">
|
||||||
|
{{ `${eventValue[0]} -> ${eventValue[1]}` }}
|
||||||
|
</div>
|
||||||
|
<pre v-else>{{ eventValue }}</pre>
|
||||||
|
</template>
|
||||||
|
<pre v-else>{{ eventValue }}</pre>
|
||||||
|
</Fieldset>
|
||||||
|
</template>
|
|
@ -181,7 +181,7 @@ const myNodeInfoChips = computed(() => {
|
||||||
const listeners = my_node_info.listeners
|
const listeners = my_node_info.listeners
|
||||||
for (const [idx, listener] of listeners?.entries()) {
|
for (const [idx, listener] of listeners?.entries()) {
|
||||||
chips.push({
|
chips.push({
|
||||||
label: `Listener ${idx}: ${listener}`,
|
label: `Listener ${idx}: ${listener.url}`,
|
||||||
icon: '',
|
icon: '',
|
||||||
} as Chip)
|
} as Chip)
|
||||||
}
|
}
|
||||||
|
@ -295,7 +295,7 @@ function showEventLogs() {
|
||||||
if (!detail)
|
if (!detail)
|
||||||
return
|
return
|
||||||
|
|
||||||
dialogContent.value = detail.events
|
dialogContent.value = detail.events.map((event: string) => JSON.parse(event))
|
||||||
dialogHeader.value = 'event_log'
|
dialogHeader.value = 'event_log'
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
@ -309,10 +309,11 @@ function showEventLogs() {
|
||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
<Timeline v-else :value="dialogContent">
|
<Timeline v-else :value="dialogContent">
|
||||||
<template #opposite="slotProps">
|
<template #opposite="slotProps">
|
||||||
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item[0])) }}</small>
|
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item.time))
|
||||||
|
}}</small>
|
||||||
</template>
|
</template>
|
||||||
<template #content="slotProps">
|
<template #content="slotProps">
|
||||||
<HumanEvent :event="slotProps.item[1]" />
|
<HumanEvent :event="slotProps.item.event" />
|
||||||
</template>
|
</template>
|
||||||
</Timeline>
|
</Timeline>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -7,6 +7,10 @@ import PrimeVue from 'primevue/config'
|
||||||
|
|
||||||
import I18nUtils from './modules/i18n'
|
import I18nUtils from './modules/i18n'
|
||||||
import * as NetworkTypes from './types/network'
|
import * as NetworkTypes from './types/network'
|
||||||
|
import HumanEvent from './components/HumanEvent.vue';
|
||||||
|
import Tooltip from 'primevue/tooltip';
|
||||||
|
import * as Api from './modules/api';
|
||||||
|
import * as Utils from './modules/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install: (app: App) => {
|
install: (app: App) => {
|
||||||
|
@ -27,7 +31,9 @@ export default {
|
||||||
|
|
||||||
app.component('Config', Config);
|
app.component('Config', Config);
|
||||||
app.component('Status', Status);
|
app.component('Status', Status);
|
||||||
|
app.component('HumanEvent', HumanEvent);
|
||||||
|
app.directive('tooltip', Tooltip);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Config, Status, I18nUtils, NetworkTypes };
|
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
|
||||||
|
|
|
@ -11,8 +11,8 @@ export interface LoginResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterResponse {
|
export interface RegisterResponse {
|
||||||
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
user: any; // 同上
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义请求体数据结构
|
// 定义请求体数据结构
|
||||||
|
@ -22,21 +22,27 @@ export interface Credential {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterData {
|
export interface RegisterData {
|
||||||
credential: Credential;
|
credentials: Credential;
|
||||||
captcha: string;
|
captcha: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiClient {
|
export interface Summary {
|
||||||
private client: AxiosInstance;
|
device_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(baseUrl: string) {
|
export class ApiClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private authFailedCb: Function | undefined;
|
||||||
|
|
||||||
|
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
|
||||||
this.client = axios.create({
|
this.client = axios.create({
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl + '/api/v1',
|
||||||
withCredentials: true, // 如果需要支持跨域携带cookie
|
withCredentials: true, // 如果需要支持跨域携带cookie
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.authFailedCb = authFailedCb;
|
||||||
|
|
||||||
// 添加请求拦截器
|
// 添加请求拦截器
|
||||||
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||||
|
@ -47,12 +53,18 @@ class ApiClient {
|
||||||
|
|
||||||
// 添加响应拦截器
|
// 添加响应拦截器
|
||||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||||
console.log('Axios Response:', response);
|
console.debug('Axios Response:', response);
|
||||||
return response.data; // 假设服务器返回的数据都在data属性中
|
return response.data; // 假设服务器返回的数据都在data属性中
|
||||||
}, (error: any) => {
|
}, (error: any) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// 请求已发出,但是服务器响应的状态码不在2xx范围
|
let response: AxiosResponse = error.response;
|
||||||
console.error('Response Error:', error.response.data);
|
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) {
|
} else if (error.request) {
|
||||||
// 请求已发出,但是没有收到响应
|
// 请求已发出,但是没有收到响应
|
||||||
console.error('Request Error:', error.request);
|
console.error('Request Error:', error.request);
|
||||||
|
@ -64,6 +76,20 @@ class ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||||
|
try {
|
||||||
|
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> {
|
public async login(data: Credential): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
|
@ -82,10 +108,24 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册
|
public async logout() {
|
||||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
await this.client.get('/auth/logout');
|
||||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
if (this.authFailedCb) {
|
||||||
return response.data;
|
this.authFailedCb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async change_password(new_password: string) {
|
||||||
|
await this.client.put('/auth/password', { new_password: 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() {
|
public async list_session() {
|
||||||
|
@ -103,6 +143,11 @@ class ApiClient {
|
||||||
return response.info.map;
|
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> {
|
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
|
||||||
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
|
||||||
config: config,
|
config: config,
|
||||||
|
@ -110,7 +155,7 @@ class ApiClient {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run_network(machine_id: string, config: string): Promise<undefined> {
|
public async run_network(machine_id: string, config: any): Promise<undefined> {
|
||||||
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
await this.client.post<string>(`/machines/${machine_id}/networks`, {
|
||||||
config: config,
|
config: config,
|
||||||
});
|
});
|
||||||
|
@ -120,8 +165,13 @@ class ApiClient {
|
||||||
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
|
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() {
|
public captcha_url() {
|
||||||
return this.client.defaults.baseURL + 'auth/captcha';
|
return this.client.defaults.baseURL + '/auth/captcha';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,3 +13,89 @@ export function num2ipv6(ip: Ipv6Addr) {
|
||||||
+ BigInt(ip.part4),
|
+ BigInt(ip.part4),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UUID {
|
||||||
|
part1: number;
|
||||||
|
part2: number;
|
||||||
|
part3: number;
|
||||||
|
part4: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UuidToStr(uuid: UUID): string {
|
||||||
|
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceInfo {
|
||||||
|
hostname: string;
|
||||||
|
public_ip: string;
|
||||||
|
running_network_count: number;
|
||||||
|
report_time: string;
|
||||||
|
easytier_version: string;
|
||||||
|
running_network_instances?: Array<string>;
|
||||||
|
machine_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeviceInfo(device: any): DeviceInfo {
|
||||||
|
let dev_info: DeviceInfo = {
|
||||||
|
hostname: device.info?.hostname,
|
||||||
|
public_ip: device.client_url,
|
||||||
|
running_network_instances: device.info?.running_network_instances.map((instance: any) => 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),
|
||||||
|
};
|
||||||
|
|
||||||
|
return dev_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write a class to run a function periodically and can be stopped by calling stop(), use setTimeout to trigger the function
|
||||||
|
export class PeriodicTask {
|
||||||
|
private interval: number;
|
||||||
|
private task: (() => Promise<void>) | undefined;
|
||||||
|
private timer: any;
|
||||||
|
|
||||||
|
constructor(task: () => Promise<void>, interval: number) {
|
||||||
|
this.interval = interval;
|
||||||
|
this.task = task;
|
||||||
|
}
|
||||||
|
|
||||||
|
_runTaskHelper(nextInterval: number) {
|
||||||
|
this.timer = setTimeout(async () => {
|
||||||
|
if (this.task) {
|
||||||
|
await this.task();
|
||||||
|
this._runTaskHelper(this.interval);
|
||||||
|
}
|
||||||
|
}, nextInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this._runTaskHelper(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.task = undefined;
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ export interface NetworkInstance {
|
||||||
export interface NetworkInstanceRunningInfo {
|
export interface NetworkInstanceRunningInfo {
|
||||||
dev_name: string
|
dev_name: string
|
||||||
my_node_info: NodeInfo
|
my_node_info: NodeInfo
|
||||||
events: Record<string, any>
|
events: Array<string>,
|
||||||
node_info: NodeInfo
|
node_info: NodeInfo
|
||||||
routes: Route[]
|
routes: Route[]
|
||||||
peers: PeerInfo[]
|
peers: PeerInfo[]
|
||||||
|
@ -104,6 +104,10 @@ export interface Ipv6Addr {
|
||||||
part4: number
|
part4: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Url {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface NodeInfo {
|
export interface NodeInfo {
|
||||||
virtual_ipv4: string
|
virtual_ipv4: string
|
||||||
hostname: string
|
hostname: string
|
||||||
|
@ -127,7 +131,7 @@ export interface NodeInfo {
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
stun_info: StunInfo
|
stun_info: StunInfo
|
||||||
listeners: string[]
|
listeners: Url[]
|
||||||
vpn_portal_cfg?: string
|
vpn_portal_cfg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/easytier.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Vue + TS</title>
|
<title>EasyTier Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
"easytier-frontend-lib": "workspace:*",
|
"easytier-frontend-lib": "workspace:*",
|
||||||
"primevue": "^4.2.1",
|
"primevue": "^4.2.1",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"vue": "^3.5.12"
|
"vue": "^3.5.12",
|
||||||
|
"vue-router": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.8.6",
|
"@types/node": "^22.8.6",
|
||||||
|
|
BIN
easytier-web/frontend/public/easytier.png
Normal file
BIN
easytier-web/frontend/public/easytier.png
Normal file
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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -2,12 +2,7 @@
|
||||||
|
|
||||||
import { I18nUtils } from 'easytier-frontend-lib'
|
import { I18nUtils } from 'easytier-frontend-lib'
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import Login from './components/Login.vue'
|
import { Toast, DynamicDialog } from 'primevue';
|
||||||
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
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await I18nUtils.loadLanguageAsync('cn')
|
await I18nUtils.loadLanguageAsync('cn')
|
||||||
|
@ -18,109 +13,10 @@ onMounted(async () => {
|
||||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="root" class="">
|
<Toast />
|
||||||
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
<DynamicDialog />
|
||||||
<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>
|
|
||||||
|
|
||||||
<aside id="logo-sidebar"
|
<RouterView />
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
BIN
easytier-web/frontend/src/assets/easytier.png
Normal file
BIN
easytier-web/frontend/src/assets/easytier.png
Normal file
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 |
33
easytier-web/frontend/src/components/ChangePassword.vue
Normal file
33
easytier-web/frontend/src/components/ChangePassword.vue
Normal file
|
@ -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>
|
65
easytier-web/frontend/src/components/Dashboard.vue
Normal file
65
easytier-web/frontend/src/components/Dashboard.vue
Normal file
|
@ -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">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import ApiClient, { ValidateConfigResponse } from '../modules/api';
|
import { Button, Column, DataTable, Drawer, ProgressSpinner, useToast } from 'primevue';
|
||||||
import { Config, Status, NetworkTypes } from 'easytier-frontend-lib'
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { Button, Column, DataTable, Drawer, Toolbar, IftaLabel, Select, Dialog, ConfirmPopup, useConfirm } from 'primevue';
|
import { Api, Utils } from 'easytier-frontend-lib';
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
api: ApiClient,
|
api: Api.ApiClient,
|
||||||
});
|
});
|
||||||
|
|
||||||
const api = props.api;
|
const api = props.api;
|
||||||
|
|
||||||
interface DeviceList {
|
const deviceList = ref<Array<Utils.DeviceInfo> | undefined>(undefined);
|
||||||
hostname: string;
|
|
||||||
public_ip: string;
|
|
||||||
running_network_count: number;
|
|
||||||
report_time: string;
|
|
||||||
easytier_version: string;
|
|
||||||
running_network_instances?: Array<string>;
|
|
||||||
machine_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedDevice = ref<DeviceList | null>(null);
|
const selectedDeviceId = computed<string | undefined>(() => route.params.deviceId as string);
|
||||||
const deviceList = ref<Array<DeviceList>>([]);
|
|
||||||
const instanceIdList = computed(() => {
|
const route = useRoute();
|
||||||
let insts = selectedDevice.value?.running_network_instances || [];
|
const router = useRouter();
|
||||||
let options = insts.map((instance: string) => {
|
const toast = useToast();
|
||||||
return { uuid: instance };
|
|
||||||
});
|
|
||||||
console.log("options", options);
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
const selectedInstanceId = ref<any | null>(null);
|
|
||||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
|
||||||
|
|
||||||
const loadDevices = async () => {
|
const loadDevices = async () => {
|
||||||
const resp = await api?.list_machines();
|
const resp = await api?.list_machines();
|
||||||
console.log(resp);
|
let devices: Array<Utils.DeviceInfo> = [];
|
||||||
let devices: Array<DeviceList> = [];
|
|
||||||
for (const device of (resp || [])) {
|
for (const device of (resp || [])) {
|
||||||
devices.push({
|
devices.push({
|
||||||
hostname: device.info?.hostname,
|
hostname: device.info?.hostname,
|
||||||
public_ip: device.client_url,
|
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,
|
running_network_count: device.info?.running_network_instances.length,
|
||||||
report_time: device.info?.report_time,
|
report_time: device.info?.report_time,
|
||||||
easytier_version: device.info?.easytier_version,
|
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;
|
deviceList.value = devices;
|
||||||
console.log(deviceList.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SelectedDevice {
|
const periodFunc = new Utils.PeriodicTask(async () => {
|
||||||
machine_id: string;
|
try {
|
||||||
instance_id: string;
|
await loadDevices();
|
||||||
}
|
} catch (e) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Load Device List Failed', detail: e, life: 2000 });
|
||||||
const checkDeviceSelected = (): SelectedDevice => {
|
console.error(e);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}, 1000);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
setInterval(loadDevices, 1000);
|
periodFunc.start();
|
||||||
setInterval(loadDeviceInfo, 1000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const visibleRight = ref(false);
|
onUnmounted(() => {
|
||||||
|
periodFunc.stop();
|
||||||
|
});
|
||||||
|
|
||||||
const showCreateNetworkDialog = ref(false);
|
const deviceManageVisible = computed<boolean>({
|
||||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
get: () => !!selectedDeviceId.value,
|
||||||
|
set: (value) => {
|
||||||
const verifyNetworkConfig = async (): Promise<ValidateConfigResponse | undefined> => {
|
if (!value) {
|
||||||
let machine_id = selectedDevice.value?.machine_id;
|
router.push({ name: 'deviceList', params: { deviceId: undefined } });
|
||||||
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 selectedDeviceHostname = computed<string | undefined>(() => {
|
||||||
|
return deviceList.value?.find((device) => device.machine_id === selectedDeviceId.value)?.hostname;
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ConfirmPopup></ConfirmPopup>
|
<div v-if="deviceList === undefined" class="w-full flex justify-center">
|
||||||
<Dialog v-model:visible="showCreateNetworkDialog" modal header="Create New Network" :style="{ width: '55rem' }">
|
<ProgressSpinner />
|
||||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
</div>
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
||||||
:sortOrder="-1">
|
:sortOrder="-1" v-if="deviceList !== undefined">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="text-xl font-bold">Device List</div>
|
<div class="text-xl font-bold">Device List</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
||||||
<Column field="public_ip" header="Public IP" style="width: 150px"></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>
|
<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 field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
||||||
<Column class="w-24 !text-end">
|
<Column class="w-24 !text-end">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Button icon="pi pi-search" @click="selectedDevice = data; visibleRight = true" severity="secondary"
|
<Button icon="pi pi-cog"
|
||||||
rounded></Button>
|
@click="router.push({ name: 'deviceManagement', params: { deviceId: data.machine_id, instanceId: data.running_network_instances[0] } })"
|
||||||
|
severity="secondary" rounded></Button>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-end">
|
||||||
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<Drawer v-model:visible="visibleRight" header="Device Management" position="right" class="w-1/2 min-w-96">
|
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
||||||
<Toolbar>
|
class="w-1/2 min-w-96">
|
||||||
<template #start>
|
<RouterView v-slot="{ Component }">
|
||||||
<IftaLabel>
|
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
||||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid"
|
</RouterView>
|
||||||
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>
|
</Drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
197
easytier-web/frontend/src/components/DeviceManagement.vue
Normal file
197
easytier-web/frontend/src/components/DeviceManagement.vue
Normal file
|
@ -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>
|
<template>
|
||||||
<div class="flex items-center justify-center min-h-screen">
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
<Card class="w-full max-w-md p-6">
|
<Card class="w-full max-w-md p-6">
|
||||||
|
@ -6,6 +68,11 @@
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<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">
|
<form v-if="!isRegistering" @submit.prevent="onSubmit" class="space-y-4">
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="username" class="block text-sm font-medium">Username</label>
|
<label for="username" class="block text-sm font-medium">Username</label>
|
||||||
|
@ -13,14 +80,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="password" class="block text-sm font-medium">Password</label>
|
<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>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button label="Login" type="submit" class="w-full" />
|
<Button label="Login" type="submit" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button label="Register" type="button" class="w-full" @click="isRegistering = true"
|
<Button label="Register" type="button" class="w-full"
|
||||||
severity="secondary" />
|
@click="$router.replace({ name: 'register' })" severity="secondary" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -32,7 +99,7 @@
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="register-password" class="block text-sm font-medium">Password</label>
|
<label for="register-password" class="block text-sm font-medium">Password</label>
|
||||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||||
class="w-full" />
|
:feedback="false" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
||||||
|
@ -43,8 +110,8 @@
|
||||||
<Button label="Register" type="submit" class="w-full" />
|
<Button label="Register" type="submit" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button label="Back to Login" type="button" class="w-full" @click="isRegistering = false"
|
<Button label="Back to Login" type="button" class="w-full"
|
||||||
severity="secondary" />
|
@click="$router.replace({ name: 'login' })" severity="secondary" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
@ -52,42 +119,4 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<style scoped></style>
|
||||||
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>
|
|
173
easytier-web/frontend/src/components/MainPage.vue
Normal file
173
easytier-web/frontend/src/components/MainPage.vue
Normal file
|
@ -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 Aura from '@primevue/themes/aura'
|
||||||
import ConfirmationService from 'primevue/confirmationservice';
|
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,
|
createApp(App).use(PrimeVue,
|
||||||
{
|
{
|
||||||
theme: {
|
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')
|
||||||
|
|
|
@ -22,5 +22,5 @@
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../frontend-lib/src/modules/api.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,9 @@ use easytier::{
|
||||||
rpc_impl::bidirect::BidirectRpcManager,
|
rpc_impl::bidirect::BidirectRpcManager,
|
||||||
rpc_types::{self, controller::BaseController},
|
rpc_types::{self, controller::BaseController},
|
||||||
web::{
|
web::{
|
||||||
HeartbeatRequest, HeartbeatResponse, RunNetworkInstanceRequest, WebClientService,
|
HeartbeatRequest, HeartbeatResponse, NetworkConfig, RunNetworkInstanceRequest,
|
||||||
WebClientServiceClientFactory, WebServerService, WebServerServiceServer,
|
WebClientService, WebClientServiceClientFactory, WebServerService,
|
||||||
|
WebServerServiceServer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tunnel::Tunnel,
|
tunnel::Tunnel,
|
||||||
|
@ -160,7 +161,13 @@ impl Session {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let req = req.unwrap();
|
let req = req.unwrap();
|
||||||
|
if req.machine_id.is_none() {
|
||||||
|
tracing::warn!(?req, "Machine id is not set, ignore");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let running_inst_ids = req
|
let running_inst_ids = req
|
||||||
.running_network_instances
|
.running_network_instances
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -187,7 +194,11 @@ impl Session {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let local_configs = match storage.db.list_network_configs(user_id, true).await {
|
let local_configs = match storage
|
||||||
|
.db
|
||||||
|
.list_network_configs(user_id, Some(req.machine_id.unwrap().into()), true)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(configs) => configs,
|
Ok(configs) => configs,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to list network configs, error: {:?}", e);
|
tracing::error!("Failed to list network configs, error: {:?}", e);
|
||||||
|
@ -206,7 +217,9 @@ impl Session {
|
||||||
BaseController::default(),
|
BaseController::default(),
|
||||||
RunNetworkInstanceRequest {
|
RunNetworkInstanceRequest {
|
||||||
inst_id: Some(c.network_instance_id.clone().into()),
|
inst_id: Some(c.network_instance_id.clone().into()),
|
||||||
config: c.network_config,
|
config: Some(
|
||||||
|
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -16,7 +16,7 @@ pub struct StorageToken {
|
||||||
pub struct StorageInner {
|
pub struct StorageInner {
|
||||||
// some map for indexing
|
// some map for indexing
|
||||||
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
|
pub token_clients_map: DashMap<String, DashSet<url::Url>>,
|
||||||
pub machine_client_url_map: DashMap<uuid::Uuid, url::Url>,
|
pub machine_client_url_map: DashMap<uuid::Uuid, DashSet<url::Url>>,
|
||||||
pub db: Db,
|
pub db: Db,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,9 @@ impl Storage {
|
||||||
|
|
||||||
self.0
|
self.0
|
||||||
.machine_client_url_map
|
.machine_client_url_map
|
||||||
.insert(stoken.machine_id, stoken.client_url.clone());
|
.entry(stoken.machine_id)
|
||||||
|
.or_insert_with(DashSet::new)
|
||||||
|
.insert(stoken.client_url.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_client(&self, stoken: &StorageToken) {
|
pub fn remove_client(&self, stoken: &StorageToken) {
|
||||||
|
@ -60,7 +62,12 @@ impl Storage {
|
||||||
set.is_empty()
|
set.is_empty()
|
||||||
});
|
});
|
||||||
|
|
||||||
self.0.machine_client_url_map.remove(&stoken.machine_id);
|
self.0
|
||||||
|
.machine_client_url_map
|
||||||
|
.remove_if(&stoken.machine_id, |_, set| {
|
||||||
|
set.remove(&stoken.client_url);
|
||||||
|
set.is_empty()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weak_ref(&self) -> WeakRefStorage {
|
pub fn weak_ref(&self) -> WeakRefStorage {
|
||||||
|
@ -71,7 +78,8 @@ impl Storage {
|
||||||
self.0
|
self.0
|
||||||
.machine_client_url_map
|
.machine_client_url_map
|
||||||
.get(&machine_id)
|
.get(&machine_id)
|
||||||
.map(|url| url.clone())
|
.map(|url| url.iter().next().map(|url| url.clone()))
|
||||||
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> {
|
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> {
|
||||||
|
|
|
@ -9,6 +9,8 @@ pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
|
#[sea_orm(column_type = "Text")]
|
||||||
|
pub device_id: String,
|
||||||
#[sea_orm(column_type = "Text", unique)]
|
#[sea_orm(column_type = "Text", unique)]
|
||||||
pub network_instance_id: String,
|
pub network_instance_id: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
|
|
|
@ -65,6 +65,7 @@ impl Db {
|
||||||
pub async fn insert_or_update_user_network_config<T: ToString>(
|
pub async fn insert_or_update_user_network_config<T: ToString>(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
user_id: UserIdInDb,
|
||||||
|
device_id: uuid::Uuid,
|
||||||
network_inst_id: uuid::Uuid,
|
network_inst_id: uuid::Uuid,
|
||||||
network_config: T,
|
network_config: T,
|
||||||
) -> Result<(), DbErr> {
|
) -> Result<(), DbErr> {
|
||||||
|
@ -81,6 +82,7 @@ impl Db {
|
||||||
.to_owned();
|
.to_owned();
|
||||||
let insert_m = urnc::ActiveModel {
|
let insert_m = urnc::ActiveModel {
|
||||||
user_id: sea_orm::Set(user_id),
|
user_id: sea_orm::Set(user_id),
|
||||||
|
device_id: sea_orm::Set(device_id.to_string()),
|
||||||
network_instance_id: sea_orm::Set(network_inst_id.to_string()),
|
network_instance_id: sea_orm::Set(network_inst_id.to_string()),
|
||||||
network_config: sea_orm::Set(network_config.to_string()),
|
network_config: sea_orm::Set(network_config.to_string()),
|
||||||
disabled: sea_orm::Set(false),
|
disabled: sea_orm::Set(false),
|
||||||
|
@ -116,6 +118,7 @@ impl Db {
|
||||||
pub async fn list_network_configs(
|
pub async fn list_network_configs(
|
||||||
&self,
|
&self,
|
||||||
user_id: UserIdInDb,
|
user_id: UserIdInDb,
|
||||||
|
device_id: Option<uuid::Uuid>,
|
||||||
only_enabled: bool,
|
only_enabled: bool,
|
||||||
) -> 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;
|
||||||
|
@ -126,6 +129,11 @@ impl Db {
|
||||||
} else {
|
} else {
|
||||||
configs
|
configs
|
||||||
};
|
};
|
||||||
|
let configs = if let Some(device_id) = device_id {
|
||||||
|
configs.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
||||||
|
} else {
|
||||||
|
configs
|
||||||
|
};
|
||||||
|
|
||||||
let configs = configs.all(self.orm_db()).await?;
|
let configs = configs.all(self.orm_db()).await?;
|
||||||
|
|
||||||
|
@ -167,8 +175,9 @@ mod tests {
|
||||||
let user_id = 1;
|
let user_id = 1;
|
||||||
let network_config = "test_config";
|
let network_config = "test_config";
|
||||||
let inst_id = uuid::Uuid::new_v4();
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
|
let device_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
db.insert_or_update_user_network_config(user_id, inst_id, network_config)
|
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -183,7 +192,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, inst_id, network_config)
|
db.insert_or_update_user_network_config(user_id, device_id, inst_id, network_config)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -193,14 +202,17 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("{:?}", result2);
|
println!("device: {}, {:?}", device_id, result2);
|
||||||
assert_eq!(result2.network_config, network_config);
|
assert_eq!(result2.network_config, network_config);
|
||||||
|
|
||||||
assert_eq!(result.create_time, result2.create_time);
|
assert_eq!(result.create_time, result2.create_time);
|
||||||
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, true).await.unwrap().len(),
|
db.list_network_configs(user_id, Some(device_id), true)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,94 +4,6 @@ use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
pub struct Migration;
|
pub struct Migration;
|
||||||
|
|
||||||
/*
|
|
||||||
-- # Entity schema.
|
|
||||||
|
|
||||||
-- Create `users` table.
|
|
||||||
create table if not exists users (
|
|
||||||
id integer primary key autoincrement,
|
|
||||||
username text not null unique,
|
|
||||||
password text not null
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create `groups` table.
|
|
||||||
create table if not exists groups (
|
|
||||||
id integer primary key autoincrement,
|
|
||||||
name text not null unique
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create `permissions` table.
|
|
||||||
create table if not exists permissions (
|
|
||||||
id integer primary key autoincrement,
|
|
||||||
name text not null unique
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- # Join tables.
|
|
||||||
|
|
||||||
-- Create `users_groups` table for many-to-many relationships between users and groups.
|
|
||||||
create table if not exists users_groups (
|
|
||||||
user_id integer references users(id),
|
|
||||||
group_id integer references groups(id),
|
|
||||||
primary key (user_id, group_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create `groups_permissions` table for many-to-many relationships between groups and permissions.
|
|
||||||
create table if not exists groups_permissions (
|
|
||||||
group_id integer references groups(id),
|
|
||||||
permission_id integer references permissions(id),
|
|
||||||
primary key (group_id, permission_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
-- # Fixture hydration.
|
|
||||||
|
|
||||||
-- Insert "user" user. password: "user"
|
|
||||||
insert into users (username, password)
|
|
||||||
values (
|
|
||||||
'user',
|
|
||||||
'$argon2i$v=19$m=16,t=2,p=1$dHJ5dXZkYmZkYXM$UkrNqWz0BbSVBq4ykLSuJw'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Insert "admin" user. password: "admin"
|
|
||||||
insert into users (username, password)
|
|
||||||
values (
|
|
||||||
'admin',
|
|
||||||
'$argon2i$v=19$m=16,t=2,p=1$Ymd1Y2FlcnQ$x0q4oZinW9S1ZB9BcaHEpQ'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Insert "users" and "superusers" groups.
|
|
||||||
insert into groups (name) values ('users');
|
|
||||||
insert into groups (name) values ('superusers');
|
|
||||||
|
|
||||||
-- Insert individual permissions.
|
|
||||||
insert into permissions (name) values ('sessions');
|
|
||||||
insert into permissions (name) values ('devices');
|
|
||||||
|
|
||||||
-- Insert group permissions.
|
|
||||||
insert into groups_permissions (group_id, permission_id)
|
|
||||||
values (
|
|
||||||
(select id from groups where name = 'users'),
|
|
||||||
(select id from permissions where name = 'devices')
|
|
||||||
), (
|
|
||||||
(select id from groups where name = 'superusers'),
|
|
||||||
(select id from permissions where name = 'sessions')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Insert users into groups.
|
|
||||||
insert into users_groups (user_id, group_id)
|
|
||||||
values (
|
|
||||||
(select id from users where username = 'user'),
|
|
||||||
(select id from groups where name = 'users')
|
|
||||||
), (
|
|
||||||
(select id from users where username = 'admin'),
|
|
||||||
(select id from groups where name = 'users')
|
|
||||||
), (
|
|
||||||
(select id from users where username = 'admin'),
|
|
||||||
(select id from groups where name = 'superusers')
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
impl MigrationName for Migration {
|
impl MigrationName for Migration {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"m20241029_000001_init"
|
"m20241029_000001_init"
|
||||||
|
@ -141,6 +53,7 @@ enum UserRunningNetworkConfigs {
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
UserId,
|
UserId,
|
||||||
|
DeviceId,
|
||||||
NetworkInstanceId,
|
NetworkInstanceId,
|
||||||
NetworkConfig,
|
NetworkConfig,
|
||||||
Disabled,
|
Disabled,
|
||||||
|
@ -273,6 +186,7 @@ impl MigrationTrait for Migration {
|
||||||
.table(UserRunningNetworkConfigs::Table)
|
.table(UserRunningNetworkConfigs::Table)
|
||||||
.col(pk_auto(UserRunningNetworkConfigs::Id).not_null())
|
.col(pk_auto(UserRunningNetworkConfigs::Id).not_null())
|
||||||
.col(integer(UserRunningNetworkConfigs::UserId).not_null())
|
.col(integer(UserRunningNetworkConfigs::UserId).not_null())
|
||||||
|
.col(text(UserRunningNetworkConfigs::DeviceId).not_null())
|
||||||
.col(
|
.col(
|
||||||
text(UserRunningNetworkConfigs::NetworkInstanceId)
|
text(UserRunningNetworkConfigs::NetworkInstanceId)
|
||||||
.unique_key()
|
.unique_key()
|
||||||
|
|
|
@ -22,6 +22,10 @@ pub struct LoginResult {
|
||||||
pub fn router() -> Router<AppStateInner> {
|
pub fn router() -> Router<AppStateInner> {
|
||||||
let r = Router::new()
|
let r = Router::new()
|
||||||
.route("/api/v1/auth/password", put(self::put::change_password))
|
.route("/api/v1/auth/password", put(self::put::change_password))
|
||||||
|
.route(
|
||||||
|
"/api/v1/auth/check_login_status",
|
||||||
|
get(self::get::check_login_status),
|
||||||
|
)
|
||||||
.route_layer(login_required!(Backend));
|
.route_layer(login_required!(Backend));
|
||||||
Router::new()
|
Router::new()
|
||||||
.merge(r)
|
.merge(r)
|
||||||
|
@ -168,4 +172,17 @@ mod get {
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_login_status(
|
||||||
|
auth_session: AuthSession,
|
||||||
|
) -> Result<Json<Void>, HttpHandleError> {
|
||||||
|
if auth_session.user.is_some() {
|
||||||
|
Ok(Json(Void::default()))
|
||||||
|
} else {
|
||||||
|
Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
Json::from(other_error("Not logged in")),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthzBackend};
|
use axum_login::{login_required, AuthManagerLayerBuilder, AuthzBackend};
|
||||||
use axum_messages::MessagesManagerLayer;
|
use axum_messages::MessagesManagerLayer;
|
||||||
use easytier::common::scoped_task::ScopedTask;
|
use easytier::common::scoped_task::ScopedTask;
|
||||||
use easytier::proto::{rpc_types};
|
use easytier::proto::rpc_types;
|
||||||
use network::NetworkApi;
|
use network::NetworkApi;
|
||||||
use sea_orm::DbErr;
|
use sea_orm::DbErr;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
@ -43,6 +43,11 @@ type AppState = State<AppStateInner>;
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
struct ListSessionJsonResp(Vec<StorageToken>);
|
struct ListSessionJsonResp(Vec<StorageToken>);
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct GetSummaryJsonResp {
|
||||||
|
device_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
message: String,
|
message: String,
|
||||||
|
@ -98,16 +103,32 @@ impl RestfulServer {
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
|
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
|
||||||
let pers = auth_session
|
let perms = auth_session
|
||||||
.backend
|
.backend
|
||||||
.get_group_permissions(auth_session.user.as_ref().unwrap())
|
.get_group_permissions(auth_session.user.as_ref().unwrap())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("{:?}", pers);
|
println!("{:?}", perms);
|
||||||
let ret = client_mgr.list_sessions().await;
|
let ret = client_mgr.list_sessions().await;
|
||||||
Ok(ListSessionJsonResp(ret).into())
|
Ok(ListSessionJsonResp(ret).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_get_summary(
|
||||||
|
auth_session: AuthSession,
|
||||||
|
State(client_mgr): AppState,
|
||||||
|
) -> Result<Json<GetSummaryJsonResp>, HttpHandleError> {
|
||||||
|
let Some(user) = auth_session.user else {
|
||||||
|
return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let machines = client_mgr.list_machine_by_token(user.tokens[0].clone());
|
||||||
|
|
||||||
|
Ok(GetSummaryJsonResp {
|
||||||
|
device_count: machines.len() as u32,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
||||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||||
|
|
||||||
|
@ -143,6 +164,7 @@ impl RestfulServer {
|
||||||
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
.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(self.network_api.build_route())
|
||||||
.route_layer(login_required!(Backend))
|
.route_layer(login_required!(Backend))
|
||||||
|
|
|
@ -9,7 +9,7 @@ use dashmap::DashSet;
|
||||||
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::rpc_types::controller::BaseController;
|
||||||
use easytier::proto::{web::*};
|
use easytier::proto::web::*;
|
||||||
|
|
||||||
use crate::client_manager::session::Session;
|
use crate::client_manager::session::Session;
|
||||||
use crate::client_manager::ClientManager;
|
use crate::client_manager::ClientManager;
|
||||||
|
@ -38,7 +38,7 @@ struct ValidateConfigJsonReq {
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
struct RunNetworkJsonReq {
|
struct RunNetworkJsonReq {
|
||||||
config: String,
|
config: NetworkConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
@ -145,7 +145,7 @@ impl NetworkApi {
|
||||||
BaseController::default(),
|
BaseController::default(),
|
||||||
RunNetworkInstanceRequest {
|
RunNetworkInstanceRequest {
|
||||||
inst_id: None,
|
inst_id: None,
|
||||||
config: config.clone(),
|
config: Some(config.clone()),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -155,8 +155,9 @@ impl NetworkApi {
|
||||||
.db()
|
.db()
|
||||||
.insert_or_update_user_network_config(
|
.insert_or_update_user_network_config(
|
||||||
auth_session.user.as_ref().unwrap().id(),
|
auth_session.user.as_ref().unwrap().id(),
|
||||||
|
machine_id,
|
||||||
resp.inst_id.clone().unwrap_or_default().into(),
|
resp.inst_id.clone().unwrap_or_default().into(),
|
||||||
config,
|
serde_json::to_string(&config).unwrap(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(convert_db_error)?;
|
.map_err(convert_db_error)?;
|
||||||
|
@ -288,6 +289,36 @@ impl NetworkApi {
|
||||||
Ok(Json(ListMachineJsonResp { machines }))
|
Ok(Json(ListMachineJsonResp { machines }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_get_network_config(
|
||||||
|
auth_session: AuthSession,
|
||||||
|
State(client_mgr): AppState,
|
||||||
|
Path((machine_id, inst_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||||
|
) -> Result<Json<NetworkConfig>, HttpHandleError> {
|
||||||
|
let inst_id = inst_id.to_string();
|
||||||
|
|
||||||
|
let db_row = client_mgr
|
||||||
|
.db()
|
||||||
|
.list_network_configs(auth_session.user.unwrap().id(), Some(machine_id), false)
|
||||||
|
.await
|
||||||
|
.map_err(convert_db_error)?
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.network_instance_id == inst_id)
|
||||||
|
.map(|x| x.network_config.clone())
|
||||||
|
.ok_or((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
other_error(format!("No such network instance: {}", inst_id)).into(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_str::<NetworkConfig>(&db_row)
|
||||||
|
.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(&mut self) -> Router<AppStateInner> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/machines", get(Self::handle_list_machines))
|
.route("/api/v1/machines", get(Self::handle_list_machines))
|
||||||
|
@ -311,5 +342,9 @@ impl NetworkApi {
|
||||||
"/api/v1/machines/:machine-id/networks/info/:inst-id",
|
"/api/v1/machines/:machine-id/networks/info/:inst-id",
|
||||||
get(Self::handle_collect_one_network_info),
|
get(Self::handle_collect_one_network_info),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/machines/:machine-id/networks/config/:inst-id",
|
||||||
|
get(Self::handle_get_network_config),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,14 @@ use tokio::{sync::broadcast, task::JoinSet};
|
||||||
|
|
||||||
pub type MyNodeInfo = crate::proto::web::MyNodeInfo;
|
pub type MyNodeInfo = crate::proto::web::MyNodeInfo;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, Clone)]
|
||||||
|
pub struct Event {
|
||||||
|
time: DateTime<Local>,
|
||||||
|
event: GlobalCtxEvent,
|
||||||
|
}
|
||||||
|
|
||||||
struct EasyTierData {
|
struct EasyTierData {
|
||||||
events: RwLock<VecDeque<(DateTime<Local>, GlobalCtxEvent)>>,
|
events: RwLock<VecDeque<Event>>,
|
||||||
node_info: RwLock<MyNodeInfo>,
|
node_info: RwLock<MyNodeInfo>,
|
||||||
routes: RwLock<Vec<Route>>,
|
routes: RwLock<Vec<Route>>,
|
||||||
peers: RwLock<Vec<PeerInfo>>,
|
peers: RwLock<Vec<PeerInfo>>,
|
||||||
|
@ -79,9 +85,12 @@ impl EasyTierLauncher {
|
||||||
async fn handle_easytier_event(event: GlobalCtxEvent, data: &EasyTierData) {
|
async fn handle_easytier_event(event: GlobalCtxEvent, data: &EasyTierData) {
|
||||||
let mut events = data.events.write().unwrap();
|
let mut events = data.events.write().unwrap();
|
||||||
let _ = data.event_subscriber.read().unwrap().send(event.clone());
|
let _ = data.event_subscriber.read().unwrap().send(event.clone());
|
||||||
events.push_back((chrono::Local::now(), event));
|
events.push_front(Event {
|
||||||
if events.len() > 100 {
|
time: chrono::Local::now(),
|
||||||
events.pop_front();
|
event: event,
|
||||||
|
});
|
||||||
|
if events.len() > 20 {
|
||||||
|
events.pop_back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,7 +276,7 @@ impl EasyTierLauncher {
|
||||||
self.data.tun_dev_name.read().unwrap().clone()
|
self.data.tun_dev_name.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_events(&self) -> Vec<(DateTime<Local>, GlobalCtxEvent)> {
|
pub fn get_events(&self) -> Vec<Event> {
|
||||||
let events = self.data.events.read().unwrap();
|
let events = self.data.events.read().unwrap();
|
||||||
events.iter().cloned().collect()
|
events.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
@ -341,7 +350,7 @@ impl NetworkInstance {
|
||||||
events: launcher
|
events: launcher
|
||||||
.get_events()
|
.get_events()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(t, e)| (t.to_string(), format!("{:?}", e)))
|
.map(|e| serde_json::to_string(e).unwrap())
|
||||||
.collect(),
|
.collect(),
|
||||||
node_info: Some(launcher.get_node_info()),
|
node_info: Some(launcher.get_node_info()),
|
||||||
routes,
|
routes,
|
||||||
|
|
|
@ -55,7 +55,7 @@ message MyNodeInfo {
|
||||||
message NetworkInstanceRunningInfo {
|
message NetworkInstanceRunningInfo {
|
||||||
string dev_name = 1;
|
string dev_name = 1;
|
||||||
MyNodeInfo my_node_info = 2;
|
MyNodeInfo my_node_info = 2;
|
||||||
map<string, string> events = 3;
|
repeated string events = 3;
|
||||||
MyNodeInfo node_info = 4;
|
MyNodeInfo node_info = 4;
|
||||||
repeated cli.Route routes = 5;
|
repeated cli.Route routes = 5;
|
||||||
repeated cli.PeerInfo peers = 6;
|
repeated cli.PeerInfo peers = 6;
|
||||||
|
@ -97,7 +97,7 @@ message ValidateConfigResponse {
|
||||||
|
|
||||||
message RunNetworkInstanceRequest {
|
message RunNetworkInstanceRequest {
|
||||||
common.UUID inst_id = 1;
|
common.UUID inst_id = 1;
|
||||||
string config = 2;
|
NetworkConfig config = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RunNetworkInstanceResponse {
|
message RunNetworkInstanceResponse {
|
||||||
|
|
|
@ -100,7 +100,10 @@ impl WebClientService for Controller {
|
||||||
_: BaseController,
|
_: BaseController,
|
||||||
req: RunNetworkInstanceRequest,
|
req: RunNetworkInstanceRequest,
|
||||||
) -> Result<RunNetworkInstanceResponse, rpc_types::error::Error> {
|
) -> Result<RunNetworkInstanceResponse, rpc_types::error::Error> {
|
||||||
let cfg = TomlConfigLoader::new_from_str(&req.config)?;
|
if req.config.is_none() {
|
||||||
|
return Err(anyhow::anyhow!("config is required").into());
|
||||||
|
}
|
||||||
|
let cfg = req.config.unwrap().gen_config()?;
|
||||||
let id = cfg.get_id();
|
let id = cfg.get_id();
|
||||||
if let Some(inst_id) = req.inst_id {
|
if let Some(inst_id) = req.inst_id {
|
||||||
cfg.set_id(inst_id.into());
|
cfg.set_id(inst_id.into());
|
||||||
|
|
|
@ -162,6 +162,9 @@ importers:
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.12
|
specifier: ^3.5.12
|
||||||
version: 3.5.12(typescript@5.6.3)
|
version: 3.5.12(typescript@5.6.3)
|
||||||
|
vue-router:
|
||||||
|
specifier: '4'
|
||||||
|
version: 4.4.5(vue@3.5.12(typescript@5.6.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.8.6
|
specifier: ^22.8.6
|
||||||
|
@ -202,6 +205,9 @@ importers:
|
||||||
aura:
|
aura:
|
||||||
specifier: link:@primevue\themes\aura
|
specifier: link:@primevue\themes\aura
|
||||||
version: link:@primevue/themes/aura
|
version: link:@primevue/themes/aura
|
||||||
|
axios:
|
||||||
|
specifier: ^1.7.7
|
||||||
|
version: 1.7.7
|
||||||
ip-num:
|
ip-num:
|
||||||
specifier: 1.5.1
|
specifier: 1.5.1
|
||||||
version: 1.5.1
|
version: 1.5.1
|
||||||
|
@ -8091,6 +8097,11 @@ snapshots:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
vue: 3.4.38(typescript@5.6.3)
|
vue: 3.4.38(typescript@5.6.3)
|
||||||
|
|
||||||
|
vue-router@4.4.5(vue@3.5.12(typescript@5.6.3)):
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 6.6.4
|
||||||
|
vue: 3.5.12(typescript@5.6.3)
|
||||||
|
|
||||||
vue-tsc@2.1.10(typescript@5.6.3):
|
vue-tsc@2.1.10(typescript@5.6.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@volar/typescript': 2.4.8
|
'@volar/typescript': 2.4.8
|
||||||
|
|
Loading…
Reference in New Issue
Block a user