reform web menu and add nats url proxy

master
veypi 1 year ago
parent cc32bd481f
commit ff8525ad2b

495
oab/Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -14,7 +14,7 @@ edition = "2021"
[dependencies]
include_dir = "*"
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "*"
serde_yaml = "*"
clap = { version = "3", features = ["derive"] }
@ -60,4 +60,5 @@ async-nats = "0.32.1"
bytes = "1.5.0"
nkeys = "0.3.2"
tracing-appender = "0.2.2"
reqwest = "0.11.22"

@ -21,9 +21,9 @@ nats_sys:
- UCOKXBGDAXXQOR4XUPUJ4O22HZ2A3KQN3JLCCYM3ISSKHLBZJXXQ3NLF
- SUAEILQZDD2UT2ZNR6DCA44YCRKAZDYDOJRUPAUA7AOWFVGSSPFPCLXF24
info:
ws_url: http://127.0.0.1:4221
nats_url: http://127.0.0.1:4222
api_url: http://127.0.0.1:4001
ws_url: 127.0.0.1:4221
nats_url: 127.0.0.1:4222
api_url: 127.0.0.1:4001
user_init_space: 300

@ -15,8 +15,13 @@ mod token;
mod upload;
mod user;
use actix_web::web;
use tracing::info;
use crate::{AppState, Result};
pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.service(info);
cfg.service(proxynats);
cfg.service(upload::save_files);
cfg.service(user::get)
.service(user::list)
@ -52,3 +57,27 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
.service(role::add)
.service(role::drop);
}
#[actix_web::get("/info")]
pub async fn info(stat: web::Data<AppState>) -> Result<impl actix_web::Responder> {
Ok(web::Json(stat.info.clone()))
}
#[actix_web::get("/nats/{p:.*}")]
pub async fn proxynats(
req: actix_web::HttpRequest,
p: web::Path<String>,
) -> Result<impl actix_web::Responder> {
let data = req.uri().query();
let p = p.into_inner();
let mut url = "http://127.0.0.1:8222".to_string();
if !p.is_empty() {
url = format!("{url}/{p}")
}
if let Some(query) = data {
url = format!("{url}?{query}")
};
info!(url);
let data = reqwest::get(url).await.unwrap().bytes().await.unwrap();
Ok(actix_web::HttpResponse::Ok().body(data))
}

@ -111,10 +111,10 @@ pub struct ApplicationConfig {
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct InfoOpt {
nats_url: String,
ws_url: String,
api_url: String,
token: Option<String>,
pub nats_url: String,
pub ws_url: String,
pub api_url: String,
pub token: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]

@ -9,4 +9,6 @@ pub mod app;
pub mod auth;
pub mod cors;
pub mod fs;
pub mod proxy;
pub mod task;
pub mod user;

@ -0,0 +1,6 @@
//
// proxy.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-19 19:31
// Distributed under terms of the MIT license.
//

@ -0,0 +1,101 @@
//
// task.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-19 01:59
// Distributed under terms of the MIT license.
//
use std::time::{Duration, Instant};
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tracing::{info, warn};
#[derive(Debug, Clone, Deserialize, Serialize)]
struct sysInfo {
client: clientInfo,
id: String,
// server: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct clientInfo {
id: i64,
acc: String,
name: String,
host: String,
}
pub fn start_nats_online(client: async_nats::client::Client) {
let db: Arc<Mutex<HashMap<i64, clientInfo>>> = Arc::new(Mutex::new(HashMap::new()));
{
let db = db.clone();
let client = client.clone();
tokio::spawn(async move {
let mut sub = client
.subscribe("$SYS.ACCOUNT.*.CONNECT".to_string())
.await
.unwrap();
while let Some(msg) = sub.next().await {
let s = String::from_utf8(msg.payload.to_vec()).unwrap();
info!("{}", s);
let inf: sysInfo = serde_json::from_slice(&msg.payload.to_vec()).unwrap();
info!("add {} {}", inf.client.id, inf.client.name);
let mut db = db.lock().unwrap();
db.insert(inf.client.id, inf.client);
}
});
}
{
let db = db.clone();
let client = client.clone();
tokio::spawn(async move {
let mut sub = client
.subscribe("$SYS.ACCOUNT.*.DISCONNECT".to_string())
.await
.unwrap();
while let Some(msg) = sub.next().await {
// let s = String::from_utf8(msg.payload.to_vec()).unwrap();
let inf: sysInfo = serde_json::from_slice(&msg.payload.to_vec()).unwrap();
info!("remove {} {}", inf.client.id, inf.client.name);
let mut db = db.lock().unwrap();
db.remove(&inf.client.id);
}
});
};
tokio::spawn(async move {
let mut sub = client.subscribe("sys.online".to_string()).await.unwrap();
while let Some(msg) = sub.next().await {
// // let s = String::from_utf8(msg.payload.to_vec()).unwrap();
// let inf: sysInfo = serde_json::from_slice(&msg.payload.to_vec()).unwrap();
// info!("remove {} {}", inf.client.id, inf.client.name);
// let mut db = db.lock().unwrap();
// db.remove(&inf.client.id);
if let Some(t) = msg.reply {
let d = {
let tmp = db.lock().unwrap();
let payload: Vec<clientInfo> = tmp.iter().map(|(_, c)| c.clone()).collect();
serde_json::to_string(&payload).unwrap()
};
match client.publish(t, d.into()).await {
Ok(_) => {}
Err(e) => {
warn!("{}", e);
}
};
}
}
});
}
pub fn start_demo() {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(5));
interval.tick().await;
let start = Instant::now();
println!("time:{:?}", start);
loop {
interval.tick().await;
println!("time:{:?}", start.elapsed());
}
});
}

@ -30,7 +30,7 @@ async fn main() -> Result<()> {
let mut data = AppState::new(&cli);
if data.debug {
std::env::set_var("RUST_LOG", "debug");
std::env::set_var("RUST_BACKTRACE", "1");
std::env::set_var("RUST_BACKTRACE", "full");
}
let _log = init_log(&data);
if cli.handle_service(data.clone())? {
@ -50,19 +50,21 @@ async fn main() -> Result<()> {
data.connect().await?;
data.connect_sqlx()?;
web(data).await?;
info!("1");
info!("12");
Ok(())
}
async fn web(data: AppState) -> Result<()> {
let client = match async_nats::ConnectOptions::new()
.nkey(data.nats_sys[1].clone())
.connect("127.0.0.1:4222")
.nkey(data.nats_usr[1].clone())
.connect(data.info.nats_url.clone())
.await
{
Ok(r) => r,
Err(e) => return Err(oab::Error::Unknown),
Err(e) => {
info!("{}", e);
return Err(oab::Error::Unknown);
}
};
libs::task::start_nats_online(client.clone());
client
.publish("msg".to_string(), Bytes::from("asd"))
.await
@ -94,7 +96,6 @@ async fn web(data: AppState) -> Result<()> {
app.wrap(logger)
.wrap(middleware::Compress::default())
.app_data(web::Data::new(data.clone()))
.service(info)
.service(fs::Files::new("/media", data.media_path.clone()).show_files_listing())
.service(
web::scope("api")
@ -169,8 +170,3 @@ async fn index(p: web::Path<String>) -> impl Responder {
.body(Asset::get("index.html").unwrap().data.into_owned()),
}
}
#[get("/info")]
pub async fn info(stat: web::Data<AppState>) -> Result<impl Responder> {
Ok(web::Json(stat.info.clone()))
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,19 @@
/*
* nats.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-19 21:36
* Distributed under terms of the MIT license.
*/
import ajax from './axios'
export default {
local: './nats/',
general() {
return ajax.get(this.local + 'varz')
},
conns() {
return ajax.get(this.local + 'connz', { subs: true })
},
}

@ -9,13 +9,14 @@
// import '@veypi/oaer'
import oaer from '@veypi/oaer'
import '@veypi/oaer/dist/index.css'
import cfg from 'src/cfg'
import bus from 'src/libs/bus'
import util from 'src/libs/util'
oaer.set({
token: util.getToken(),
host: 'http://' + window.location.host,
uuid: 'FR9P5t8debxc11aFF',
host: cfg.host,
uuid: cfg.id,
})
bus.on('token', (t: any) => {

@ -0,0 +1,15 @@
/*
* cfg.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-18 22:03
* Distributed under terms of the MIT license.
*/
const cfg = {
host: 'http://' + window.location.host,
id: 'FR9P5t8debxc11aFF',
}
export default cfg

@ -1,12 +1,8 @@
<template>
<q-item class="flex items-center" v-ripple clickable tag="a" :href="link" :to="to">
<!-- <q-item-section v-if="icon" avatar> -->
<!-- </q-item-section> -->
<q-icon size="1.5rem" class="mr-2" :name="icon" />
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<!-- <q-item-label caption>{{ caption }}</q-item-label> -->
</q-item-section>
</q-item>
</template>

@ -1,30 +0,0 @@
<!--
* main.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-04 10:56
* Distributed under terms of the MIT license.
-->
<template>
<q-list>
<!-- <q-item-label header> -->
<!-- Essential Links -->
<!-- </q-item-label> -->
<EssentialLink v-for="link in menu.list" :key="link.title" v-bind="link" />
</q-list>
</template>
<script lang="ts" setup>
import EssentialLink from 'src/components/EssentialLink.vue';
import { useMenuStore } from 'src/stores/menu';
import { onMounted } from 'vue';
let menu = useMenuStore()
onMounted(() => {
console.log('loading main menu')
})
</script>
<style scoped></style>

@ -24,13 +24,12 @@
<script lang="ts" setup>
import msg from '@veypi/msg';
import api from 'src/boot/api';
import { MenuLink, modelsApp } from 'src/models';
import { useMenuStore } from 'src/stores/menu';
import { modelsApp } from 'src/models';
import { computed, watch, ref, onMounted, provide, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { RouteLocationNamedRaw } from 'vue-router';
import menus from './menus'
let route = useRoute();
let menu = useMenuStore()
let id = computed(() => route.params.id)
let app = ref({} as modelsApp)
@ -38,61 +37,40 @@ let app = ref({} as modelsApp)
provide('app', app)
const sync_app = () => {
let tid = id.value as string
api.app.get(id.value as string).then((e: modelsApp) => {
app.value = e
Links.value[1].title = e.name
for (let i in Links.value) {
let l: RouteLocationNamedRaw = Links.value[i].to as any
let links = menus.appLinks.value.concat([])
links[0].title = e.name
if (menus.uniqueLinks[tid]?.length) {
for (let r of menus.uniqueLinks[tid]) {
links.splice(1, 0, r)
}
}
for (let i in links) {
let l: RouteLocationNamedRaw = links[i].to as any
if (l.params) {
l.params.id = e.id
} else {
l.params = { id: e.id }
}
}
menus.items.value = links
}).catch(e => {
msg.Warn('sync app data failed: ' + e)
})
}
const Links = ref([
{
title: '应用中心',
caption: '',
icon: 'v-apps',
to: { name: 'home' }
},
{
title: '',
caption: '',
icon: 'v-home',
to: { name: 'app.home', params: { id: id.value } }
},
{
title: '用户管理',
icon: 'v-team',
to: { name: 'app.user', params: { id: id.value } }
},
{
title: '权限管理',
icon: 'v-key',
to: { name: 'app.auth', params: { id: id.value } }
},
{
title: '应用设置',
caption: '',
icon: 'v-setting',
to: { name: 'app.settings', params: { id: id.value } }
},
] as MenuLink[])
watch(id, (e) => {
console.log(e)
if (e) {
sync_app()
}
})
}, { immediate: true })
onMounted(() => {
sync_app()
menu.set(Links.value)
})
onBeforeUnmount(() => {
menu.load_default()
menus.load_default()
})
</script>

@ -56,7 +56,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import Menu from 'src/components/menu.vue'
import Menu from './menu.vue'
import { useUserStore } from 'src/stores/user';
import { OAer } from "@veypi/oaer";
import { util } from 'src/libs';

@ -0,0 +1,35 @@
<!--
* main.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-04 10:56
* Distributed under terms of the MIT license.
-->
<template>
<q-list>
<!-- <q-item-label header> -->
<!-- Essential Links -->
<!-- </q-item-label> -->
<template v-for="link in items" :key="link.title">
<q-item class="flex items-center" v-ripple clickable tag="a" :to="link.to">
<q-icon size="1.5rem" class="mr-2" :name="link.icon" />
<q-item-section>
<q-item-label>{{ link.title }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-list>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import menus from './menus'
const items = computed(() => menus.items.value)
</script>
<style scoped></style>

@ -0,0 +1,89 @@
/*
* menu.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-18 22:58
* Distributed under terms of the MIT license.
*/
import { Dict } from 'src/models';
import { ref } from 'vue';
import { RouteLocationRaw } from 'vue-router';
import cfg from 'src/cfg'
export interface MenuLink {
title: string;
caption?: string;
to?: RouteLocationRaw;
link?: string;
icon?: string;
router?: any;
}
const tmp_router = (title: string, icon: string, path: string, com:
string) => {
let name = 'app.' + path
return {
title: title,
icon: icon,
name: name,
to: { name: name, params: { id: '' } },
router: {
path: path,
name: name,
component: () => import('../pages/app/' + com + '.vue')
},
}
}
let uniqueLinks: { [key: string]: [MenuLink] } = {
[cfg.id]: [tmp_router('系统信息', 'v-data-view', 'oasys', 'oasys')]
}
const defaultLinks: MenuLink[] = [
{
title: '应用中心',
caption: '',
icon: 'v-apps',
to: { name: 'home' }
},
{
title: '文件管理',
caption: '',
icon: 'v-folder',
to: { name: 'fs' }
},
{
title: '账号设置',
icon: 'v-user',
to: { name: 'user' }
},
{
title: '文档中心',
icon: 'v-file-exception',
to: { name: 'doc' }
},
{
title: '设置',
caption: '',
icon: 'v-setting',
to: { name: 'settings' }
},
]
const items = ref(defaultLinks)
const load_default = () => {
items.value = defaultLinks
}
const appLinks = ref([
tmp_router('', 'v-home', 'home', 'home'),
tmp_router('用户管理', 'v-team', 'user', 'user'),
tmp_router('权限管理', 'v-key', 'auth', 'auth'),
tmp_router('应用设置', 'v-setting', 'cfg', 'cfg'),
tmp_router('test', 'v-key', 'test', '../IndexPage'),
] as MenuLink[])
export default { items, load_default, appLinks, uniqueLinks }

@ -8,7 +8,10 @@ function padLeftZero(str: string): string {
const util = {
datetostr(d: string) {
let r = new Date(d + 'z')
if (!d.endsWith('Z')) {
d = d + 'Z'
}
let r = new Date(d)
let delta = (new Date().getTime() - r.getTime()) / 1000
if (delta < 0) {
} else if (delta < 60) {

@ -5,7 +5,6 @@
* @descriptionindex
*/
import { RouteLocationRaw } from 'vue-router';
import { AccessLevel } from './auth';
export { type Auths, type modelsSimpleAuth, NewAuths, R, AccessLevel, LevelOptions } from './auth'
@ -56,14 +55,6 @@ export interface DocGroup {
items?: DocItem[]
}
export interface MenuLink {
title: string;
caption?: string;
to?: RouteLocationRaw;
link?: string;
icon?: string;
}
export interface modelsBread {
Index: number
Name: string

@ -0,0 +1,79 @@
<!--
* oasys.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-18 23:45
* Distributed under terms of the MIT license.
-->
<template>
<div>
<div v-if="data.server?.id">
<div class="text-2xl mb-4">
消息服务
<div class="float-right text-sm">{{ new Date(data.server.time).toLocaleString() }}</div>
</div>
<div class="">
<div class="w-full">ID: {{ data.server?.id }}</div>
<div class="flex gap-8">
<div>CPU占用: {{ data.statsz.cpu }}%</div>
<div>内存占用: {{ (data.statsz.mem / 1024 / 1024).toFixed(2) }}M</div>
<div>连接数: {{ data.statsz.connections }}</div>
</div>
<div>发送: {{ (send_received[1] / 1024).toFixed(2) }} KB/s</div>
<div>收到: {{ (send_received[0] / 1024).toFixed(2) }} KB/s</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch, onUnmounted } from 'vue';
import { nats } from '@veypi/oaer'
const data = ref({} as any)
const id = computed(() => data.value.server?.id)
const subs: any[] = []
const timer = ref()
let old_data = [0, 0]
const send_received = computed(() => {
if (!id.value) {
return [0, 0]
}
let os = data.value.statsz.sent.bytes
let or = data.value.statsz.received.bytes
let res = [os - old_data[0], or - old_data[1]]
old_data = [os, or]
return res
})
watch(id, (_) => {
timer.value = setInterval(() => {
nats.request('$SYS.REQ.SERVER.PING').then((m) => {
data.value = JSON.parse(m)
})
}, 1000)
})
watch(computed(() => nats.ready.value), e => {
if (e) {
nats.request('$SYS.REQ.SERVER.PING').then((m) => {
data.value = JSON.parse(m)
let os = data.value.statsz.sent.bytes
let or = data.value.statsz.received.bytes
old_data = [os, or]
})
}
}, { immediate: true })
onUnmounted(() => {
clearInterval(timer.value)
for (let i of subs) {
i.unsubscribe()
}
})
</script>
<style scoped></style>

@ -1,5 +1,6 @@
import { Auths } from 'src/models/auth';
import { RouteRecordRaw } from 'vue-router';
import menus from 'src/layouts/menus'
declare module 'vue-router' {
interface RouteMeta {
@ -17,58 +18,48 @@ function loadcomponents(path: string, name: string, main: string) {
return {
path: path,
name: name,
components: {
default: () => import("../pages/" + main + ".vue"),
}
component: () => import("../pages/" + main + ".vue"),
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('../layouts/MainLayout.vue'),
component: () => import('src/layouts/MainLayout.vue'),
meta: {
requiresAuth: true,
},
redirect: 'home',
children: [
loadcomponents('home', 'home', 'IndexPage'),
loadcomponents('user', 'user', 'user'),
loadcomponents('fs', 'fs', 'fs'),
loadcomponents('doc', 'doc', 'doc'),
loadcomponents('doc/:typ/:url(.*)', 'doc_item', 'docItem'),
loadcomponents('settings', 'settings', 'settings'),
{
path: 'app/:id',
name: 'app',
component: () => import("../layouts/AppLayout.vue"),
redirect: { name: 'app.home' },
children: [
loadcomponents('home', 'app.home', 'AppHome'),
loadcomponents('user', 'app.user', 'AppUser'),
loadcomponents('auth', 'app.auth', 'AppAuth'),
loadcomponents('settings', 'app.settings', 'AppCfg'),
]
}
},
loadcomponents('home', 'home', 'IndexPage'),
loadcomponents('user', 'user', 'user'),
loadcomponents('fs', 'fs', 'fs'),
loadcomponents('doc', 'doc', 'doc'),
loadcomponents('doc/:typ/:url(.*)', 'doc_item', 'docItem'),
loadcomponents('settings', 'settings', 'settings'),
],
},
{
path: '/login/:uuid?',
name: 'login',
component: () => import('../pages/login.vue'),
},
{
path: '/register/:uuid?',
name: 'register',
component: () => import('../pages/register.vue'),
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('../pages/404.vue'),
},
loadcomponents('/login/:uuid?', 'login', 'login'),
loadcomponents('/register/:uuid?', 'register', 'register'),
loadcomponents('/:catchAll(.*)*', '404', '404')
];
for (let i of menus.appLinks.value) {
// @ts-ignore
routes[0].children[0].children.push(i.router)
}
for (let i in menus.uniqueLinks) {
for (let j of menus.uniqueLinks[i]) {
// @ts-ignore
routes[0].children[0].children.push(j.router)
}
}
export default routes;

@ -17,13 +17,15 @@ accounts: {
usrs: {
users: [
{ nkey: UCXFAAVMCPTATZUZX6H24YF6FI3NKPQBPLM6BNN2EDFPNSUUEZPNFKEL},
{ user: cli, password: cli},
],
exports: [
{stream: pub.>},
{service: psrv.>},
# {stream: node.>},
# {service: node.>},
],
imports: [
{stream: {account: SYS, subject: usr.>}, prefix: a},
{stream: {account: SYS, subject: >}},
{service: {account: SYS, subject: >}},
],
},
nodes: {
@ -31,20 +33,21 @@ accounts: {
{ nkey: UAU6HPAHVIQWODQ365HMSHGZPSXJHR35T6ACURR3STGXFZNWXFNG5EA6},
],
exports: [
{stream: pub.>},
{service: psrv.>},
# {stream: >},
# {service: >},
],
imports: [
# {stream: {account: usrs, subject: node.>}, prefix: 'usr'},
# {service: {account: usrs, subject: >}},
],
},
SYS: {
users: [
{ nkey: UCOKXBGDAXXQOR4XUPUJ4O22HZ2A3KQN3JLCCYM3ISSKHLBZJXXQ3NLF},
{ user: cli, password: cli},
],
exports: [
{stream: usr.>},
{stream: node.>},
{service: usr.>},
{service: node.>},
{stream: >},
{service: >},
],
},
}

Loading…
Cancel
Save