master
veypi 1 year ago
parent 583bca4817
commit 73e56ca8c5

@ -4,3 +4,11 @@
[Demo](https://oa.veypi.com)
### 依赖库
```bash
docker run -dit --name=tsdb -v /Users/veypi/test/vdb:/victoria-metrics-data -p 8428:8428 victoriametrics/victoria-metrics -search.latencyOffset=1s
nats-server -c ./script/nats.cfg
```

@ -21,9 +21,10 @@ nats_sys:
- UCOKXBGDAXXQOR4XUPUJ4O22HZ2A3KQN3JLCCYM3ISSKHLBZJXXQ3NLF
- SUAEILQZDD2UT2ZNR6DCA44YCRKAZDYDOJRUPAUA7AOWFVGSSPFPCLXF24
info:
ws_url: 198.19.249.3:4221
nats_url: 198.19.249.3:4222
api_url: 198.19.249.3:4001
ws_url: 127.0.0.1:4221
nats_url: 127.0.0.1:4222
api_url: 127.0.0.1:4001
ts_url: 127.0.0.1:8428
user_init_space: 300

@ -1,3 +1,3 @@
<!DOCTYPE html><html><head><title>OA</title><meta charset=utf-8><meta name=description content=oneauth><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/ico href="/favicon.ico"> <script type="module" crossorigin src="/assets/index.80ecf914.js"></script>
<link rel="stylesheet" href="/assets/index.45343e6a.css">
<!DOCTYPE html><html><head><title>OA</title><meta charset=utf-8><meta name=description content=oneauth><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/ico href="/favicon.ico"> <script type="module" crossorigin src="/assets/index.559d1c65.js"></script>
<link rel="stylesheet" href="/assets/index.4abc03f5.css">
</head><body><div id=v-msg></div><div id=q-app></div></body></html>

@ -114,6 +114,7 @@ pub struct InfoOpt {
pub nats_url: String,
pub ws_url: String,
pub api_url: String,
pub ts_url: String,
pub token: Option<String>,
}
@ -193,9 +194,10 @@ impl AppState {
],
user_init_space: 300,
info: InfoOpt {
ws_url: "http://127.0.0.1:4221".to_string(),
nats_url: "http://127.0.0.1:4222".to_string(),
api_url: "http://127.0.0.1:4001".to_string(),
ws_url: "127.0.0.1:4221".to_string(),
nats_url: "127.0.0.1:4222".to_string(),
api_url: "127.0.0.1:4001".to_string(),
ts_url: "127.0.0.1:8428".to_string(),
token: None,
},
}

@ -13,20 +13,20 @@ use std::sync::{Arc, Mutex};
use tracing::{info, warn};
#[derive(Debug, Clone, Deserialize, Serialize)]
struct sysInfo {
client: clientInfo,
struct SysInfo {
client: ClientInfo,
id: String,
// server: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct clientInfo {
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: Arc<Mutex<HashMap<i64, ClientInfo>>> = Arc::new(Mutex::new(HashMap::new()));
{
let db = db.clone();
let client = client.clone();
@ -38,7 +38,7 @@ pub fn start_nats_online(client: async_nats::client::Client) {
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();
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);
@ -55,7 +55,7 @@ pub fn start_nats_online(client: async_nats::client::Client) {
.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();
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);
@ -66,14 +66,14 @@ pub fn start_nats_online(client: async_nats::client::Client) {
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();
// 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();
let payload: Vec<ClientInfo> = tmp.iter().map(|(_, c)| c.clone()).collect();
serde_json::to_string(&payload).unwrap()
};
match client.publish(t, d.into()).await {

@ -5,12 +5,9 @@
// Distributed under terms of the Apache license.
//
use bytes::Bytes;
use actix_files as fs;
use actix_web::{
dev::{self, Service},
get,
http::StatusCode,
middleware::{self, ErrorHandlerResponse, ErrorHandlers},
web::{self},
@ -53,22 +50,7 @@ async fn main() -> Result<()> {
Ok(())
}
async fn web(data: AppState) -> Result<()> {
let client = match async_nats::ConnectOptions::new()
.nkey(data.nats_usr[1].clone())
.connect(data.info.nats_url.clone())
.await
{
Ok(r) => r,
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
.unwrap();
let url = data.server_url.clone();
let dav = libs::fs::core();
let serv = HttpServer::new(move || {

@ -40,7 +40,6 @@ module.exports = configure(function(/* ctx */) {
'i18n',
'api',
'pack',
'oaer',
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css

File diff suppressed because one or more lines are too long

@ -1,24 +0,0 @@
/*
* oaer.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-16 21:20
* Distributed under terms of the MIT license.
*/
// 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: cfg.host,
uuid: cfg.id,
})
bus.on('token', (t: any) => {
oaer.set({ token: t })
})

@ -1,18 +0,0 @@
<template>
<q-item class="flex items-center" v-ripple clickable tag="a" :href="link" :to="to">
<q-icon size="1.5rem" class="mr-2" :name="icon" />
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
import { MenuLink } from 'src/models'
withDefaults(defineProps<MenuLink>(), {
caption: '',
icon: '',
});
</script>

@ -1,37 +0,0 @@
<template>
<div>
<p>{{ title }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id" @click="increment">
{{ todo.id }} - {{ todo.content }}
</li>
</ul>
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
<p>Active: {{ active ? 'yes' : 'no' }}</p>
<p>Clicks on todos: {{ clickCount }}</p>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Todo, Meta } from './models';
interface Props {
title: string;
todos?: Todo[];
meta: Meta;
active: boolean;
}
const props = withDefaults(defineProps<Props>(), {
todos: () => [],
});
const clickCount = ref(0);
function increment() {
clickCount.value += 1;
return clickCount.value;
}
const todoCount = computed(() => props.todos.length);
</script>

@ -10,7 +10,7 @@
class="cursor-pointer rounded-full h-8 pr-4 flex items-center hover:bg-gray-100" @click="toggle">
<q-icon class="transition-all mx-2" :class="[expand ? 'rotate-90' :
'']" style="font-size: 24px;" :name="root.type ===
'directory' ? 'v-caret-right' : 'v-file'"> </q-icon>
'directory' ? 'v-right' : 'v-file'"> </q-icon>
<div>
{{ root.basename || '/' }}
</div>

@ -0,0 +1,48 @@
<!--
* tooltip.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-23 21:49
* Distributed under terms of the MIT license.
-->
<template>
<div class="v-tooltip">
<slot></slot>
<div class="v-tooltip-text">
<slot name="text">
{{ text }}
</slot>
</div>
</div>
</template>
<script lang="ts" setup>
withDefaults(defineProps<{
text?: string
}>(),
{}
)
</script>
<style scoped>
.v-tooltip {
position: relative;
display: inline-block;
}
.v-tooltip .v-tooltip-text {
visibility: hidden;
visibility: hidden;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
text-align: center;
padding: 5px 5px;
border-radius: 6px;
position: absolute;
z-index: 1;
}
.v-tooltip:hover .v-tooltip-text {
visibility: visible;
}
</style>

@ -0,0 +1,11 @@
/*
* index.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-22 05:11
* Distributed under terms of the MIT license.
*/
import tschart from './tschart.vue'
export default tschart

@ -0,0 +1,71 @@
/*
* params.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-22 05:13
* Distributed under terms of the MIT license.
*/
import { ref } from 'vue'
let mode = ref(0)
let mode_label = ['近5分钟', '近1小时', '近24小时', '近7天', '近30天']
let change_mode = (m: number) => {
mode.value = m
let now = new Date()
switch (m) {
case 0: {
now.setMinutes(now.getMinutes() - 5)
params.value.start = now
params.value.step = "2s"
break
}
case 1: {
now.setHours(now.getHours() - 1)
params.value.start = now
params.value.step = "10s"
break
}
case 2: {
now.setHours(now.getHours() - 24)
params.value.start = now
params.value.step = "20s"
break
}
case 3: {
now.setHours(now.getHours() - 24 * 7)
params.value.start = now
params.value.step = "30s"
break
}
case 4: {
now.setHours(now.getHours() - 24 * 29)
params.value.start = now
params.value.step = "1h"
break
}
case 5: {
break
}
}
}
let params = ref<{ start: Date, end: Date, step: string }>({
start: new Date(),
end: new Date(),
step: '2s'
})
change_mode(0)
const set_delta = (start?: Date, end?: Date) => {
if (start) {
params.value.start = start
}
if (end) {
params.value.end = end
}
let delta = params.value.end.getTime() -
params.value.start.getTime()
console.log(delta)
}
export { params, change_mode, mode, mode_label }

@ -6,13 +6,22 @@
-->
<template>
<div class="w-full h-full">
<div class="v-chart w-full h-full" ref="chartdom"></div>
<div class="h-16 flex justify-start items-center">
<q-chip clickable :color="enable_sync ? 'primary' : ''" @click="enable_sync = !enable_sync">{{ enable_sync ? '关闭同步'
:
'开启同步' }}</q-chip>
<q-chip clickable :color="mode === k ? 'primary' : ''" v-for="(v, k) in mode_label" :key="k"
@click="change_mode(k)">{{
v }}</q-chip>
</div>
<div class="v-chart w-full" style="height: calc(100% - 4rem);" ref="chartdom"></div>
</div>
</template>
<script lang="ts" setup>
import * as echart from 'echarts'
import api from 'src/boot/api';
import { params, mode, change_mode, mode_label } from './params'
import { onMounted, onUnmounted, computed, ref, watch, markRaw } from 'vue';
interface Item {
@ -24,22 +33,14 @@ interface Item {
}
let props = withDefaults(defineProps<{
item: Item,
// start?: string,
// end?: string,
// step?: string
sync?: boolean,
}>(),
{
}
)
let getparams = ref<any>({
start: () => {
let d = new Date()
d.setMinutes(d.getMinutes() - 3)
return d.toISOString()
}, end: undefined, step: '2s'
})
let count = 0
let timer = ref<any[]>([])
let enable_sync = ref(false)
let chartdom = ref()
let options = ref<{ [key: string]: any }>({})
let chart: echart.ECharts = {} as any
@ -58,6 +59,9 @@ const init_chart = () => {
if (chart.clear) {
chart.clear()
}
timer.value.forEach(e => {
clearInterval(e)
})
options.value = {
animationThreshold: 200,
tooltip: Object.assign({}, tooltip),
@ -70,23 +74,30 @@ const init_chart = () => {
xAxis: {
type: 'time',
},
dataZoom: [
{
type: 'slider',
xAxisIndex: [0],
filterMode: 'filter'
},
],
yAxis: {},
series: []
}
if (props.item.valueFormatter) {
options.value.tooltip.valueFormatter = props.item.valueFormatter
}
let tmp = {} as any
if (getparams.value.start) {
tmp.start = getparams.value.start()
let tmp = {
start: params.value.start.toISOString(),
step: params.value.step,
}
if (getparams.value.step) {
tmp.step = getparams.value.step
}
let query: string[] = Array.isArray(props.item.query) ? props.item.query :
let querys: string[] = Array.isArray(props.item.query) ? props.item.query :
[props.item.query]
for (let q = 0; q < query.length; q++) {
api.tsdb.range(query[q], tmp).then(e => {
let labels = Array.isArray(props.item.label) ? props.item.label :
[props.item.label]
for (let q = 0; q < querys.length; q++) {
let query = querys[q]
api.tsdb.range(query, tmp).then(e => {
if (e.status == 'success') {
let data = e.data.result as any[]
if (data.length == 0) {
@ -96,19 +107,19 @@ const init_chart = () => {
let idx = options.value.series.length || 0
data.forEach(d => {
let name = props.item.name
if (typeof props.item.label === 'string') {
name = props.item.label
} else if (typeof props.item.label === 'function') {
name = props.item.label(d.metric)
} else if (Array.isArray(props.item.label)) {
name = props.item.label[q]
let label = labels[q]
if (typeof label === 'string') {
name = label
} else if (typeof label === 'function') {
name = label(d.metric)
}
options.value.series.push({
name: name,
data: d.values.map((e: any) =>
[e[0] * 1000, Number(e[1])]),
metric: d.metric,
origin: query[q],
metric_str: JSON.stringify(d.metric),
origin: query,
symbol: 'none',
smooth: true,
type: 'line',
@ -116,7 +127,7 @@ const init_chart = () => {
})
chart.setOption(options.value)
let t = setInterval(() => {
sync_chart(idx, query[q], count)
sync_chart(idx, query, count)
}, 1000)
timer.value.push(t)
}
@ -125,16 +136,30 @@ const init_chart = () => {
// let query = props.query
}
const sync_chart = (idx: number, query: string, c: number) => {
if (!enable_sync.value) {
return
}
api.tsdb.query(query).then(e => {
if (e.status == 'success') {
let data = e.data.result as any[]
if (data.length == 0) {
console.warn('not get data')
timer.value.forEach(e => {
clearInterval(e)
})
return
}
if (count === c) {
data.forEach((d, i) => {
options.value.series[idx + i].data.push([d.value[0] * 1000,
let sidx = idx + i
if (d.metric) {
let ti = options.value.series.findIndex((s: any) =>
query === s.origin && JSON.stringify(d.metric) === s.metric_str)
if (ti >= 0) {
sidx = ti
}
}
options.value.series[sidx].data.push([d.value[0] * 1000,
Number(d.value[1])])
})
chart.setOption(options.value)
@ -143,17 +168,22 @@ const sync_chart = (idx: number, query: string, c: number) => {
})
}
watch(computed(() => props.item), q => {
timer.value.forEach(e => {
clearInterval(e)
})
if (q) {
init_chart()
}
}, { immediate: true })
})
watch(mode, q => {
init_chart()
})
onMounted(() => {
chart = markRaw(echart.init(chartdom.value))
enable_sync.value = props.sync
chart = markRaw(echart.init(chartdom.value, null, { renderer: 'svg' }))
init_chart()
})
onUnmounted(() => {
timer.value.forEach(e => {

@ -61,7 +61,20 @@ import { useRouter } from 'vue-router';
import Menu from './menu.vue'
import { useUserStore } from 'src/stores/user';
import { OAer } from "@veypi/oaer";
import { util } from 'src/libs';
import { util, bus } from 'src/libs';
import oaer from '@veypi/oaer'
import '@veypi/oaer/dist/index.css'
import cfg from 'src/cfg'
oaer.set({
token: util.getToken(),
host: cfg.host,
uuid: cfg.id,
})
bus.on('token', (t: any) => {
oaer.set({ token: t })
})
const user = useUserStore()
const router = useRouter()

@ -16,13 +16,17 @@
<script lang="ts" setup>
import FsTree from 'src/components/FsTree.vue';
import { oafs, fileProps } from '@veypi/oaer';
import { onMounted, ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
let root = ref({} as fileProps)
onMounted(() => {
watch(oafs.ready, e => {
if (e) {
oafs.dav().stat('/').then(e => {
root.value = e as fileProps
})
}
}, { immediate: true })
onMounted(() => {
})
</script>

@ -5,8 +5,38 @@
* Distributed under terms of the MIT license.
-->
<template>
<div>
<div v-if="id">
<div class="text-2xl mb-4">
消息服务
<div class="float-right text-sm">{{ new Date(data.now).toLocaleString() }}</div>
</div>
<div class="">
<div class="w-full">ID: {{ id }}</div>
<div class="flex gap-8">
<div>CPU占用: {{ data.cpu }}%</div>
<div>内存占用: {{ (data.mem / 1024 / 1024).toFixed(2) }}M</div>
<div>连接数: {{ data.connections }}</div>
</div>
<div>发送: {{ (send_received[0] / 1024).toFixed(2) }} KB/s</div>
<div>收到: {{ (send_received[1] / 1024).toFixed(2) }} KB/s</div>
</div>
<div class="grid grid-cols-4 gap-4 mt-10" v-if="conns.length">
<div>ID</div>
<div>Name</div>
<div>运行时间</div>
<div>订阅主题</div>
<template v-for="c of conns" :key="c.cid">
<div>{{ c.cid }}</div>
<div>{{ c.name || '无' }}</div>
<div>{{ new Date(c.start).toLocaleString() }}</div>
<div>{{ c.subscriptions_list ?
c.subscriptions_list.sort().join(' ') : '' }}</div>
</template>
</div>
</div>
<div class="flex flex-nowrap">
<div class="grow" style="height: calc(100%);">
<div class="grow" style="min-height: 50vh;">
<tschart :item="querys[idx]"></tschart>
</div>
<div class="flex flex-col gap-5">
@ -15,18 +45,78 @@
q.name }} </q-chip>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import tschart from 'src/components/tschart.vue';
import { ref } from 'vue';
import { computed, ref, watch, onUnmounted } from 'vue';
import { nats } from '@veypi/oaer'
import api from 'src/boot/api'
const data = ref({} as any)
const conns = ref<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.out_bytes
let or = data.value.in_bytes
let res = [os - old_data[0], or - old_data[1]]
old_data = [os, or]
return res
})
watch(id, (_) => {
timer.value = setInterval(() => {
api.nats.general().then(e => {
data.value = e
})
api.nats.conns().then(e => {
conns.value = e.connections
})
// nats.request('$SYS.REQ.SERVER.PING').then((m) => {
// data.value = JSON.parse(m)
// })
}, 1000)
})
watch(computed(() => nats.ready.value), e => {
if (e) {
api.nats.general().then(e => {
old_data = [e.out_bytes, e.in_bytes]
data.value = e
})
}
}, { immediate: true })
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
for (let i of subs) {
i.unsubscribe()
}
})
import tschart from 'src/components/tschart';
const idx = ref(0)
const querys = ref([
const querys = ref<{
name: string, query: string[] | string, label?: any,
valueFormatter?: any
}[]>([
{
name: 'cpu占用',
query: `100 - avg (irate(node_cpu_seconds_total{mode="idle"}[3s]))
by(id) * 100`,
// query: `100 - avg (irate(node_cpu_seconds_total{mode="idle"}[3s]))
// by(id) * 100`,
query: `avg by
(id)(irate(node_cpu_seconds_total{mode=~"sytem|user|iowait|irq|softirq|nice|steal|guest"}[3s]))
* 100`,
label: (d: any) => d.id as string,
valueFormatter: (value: number) => value.toFixed(2) + "%",
},
@ -40,28 +130,59 @@ const querys = ref([
valueFormatter: (value: number) => value.toFixed(2) + "%",
},
{
name: 'linux 内存',
query: [
`(node_memory_Buffers_bytes + node_memory_Cached_bytes +
node_memory_MemFree_bytes) / 1024 / 1024 / 1024`,
`node_memory_MemTotal_bytes / 1024 /1024 / 1024`
],
label: ['使用内存', '总内存'],
valueFormatter: (value: number) => value.toFixed(2) + "GB",
name: '磁盘',
query: `(1 - avg(node_filesystem_avail_bytes /
node_filesystem_size_bytes[3s]) by (device, id)) * 100 `,
label: (d: any) => `${d.id}: ${d.device}` as string,
valueFormatter: (value: number) => value.toFixed(2) + "%",
},
{
name: 'Mac cpu频率',
query: 'node_cpu_seconds_total',
label: (d: any) => `cpu: ${d.cpu} mode: ${d.mode}` as string
name: '磁盘IOPS',
query: [
`sum by (id) (rate(node_disk_reads_completed_total[3s]))`,
`sum by (id) (rate(node_disk_writes_completed_total[3s]))`,
],
label: [
(d: any) => `${d.id}`,
(d: any) => `${d.id}`,
],
},
{
name: 'mem',
query: 'node_memory_free_bytes / 1024 / 1024 / 1024'
name: '网络带宽',
query: [
`sum by(id)(irate(node_network_receive_bytes_total{device!~"bond.*?|lo"}[3s])) / 1048576`,
`sum by(id)(irate(node_network_transmit_bytes_total{device!~"bond.*?|lo"}[3s])) / 1048576`
],
label: [
(d: any) => `${d.id} 下行`,
(d: any) => `${d.id} 上行`,
],
valueFormatter: (value: number) => value.toFixed(2) + "MB/s",
},
{
name: 'Mac swap',
query: 'node_memory_swap_used_bytes / 1024 / 1024 '
name: '内存',
query: [
`(node_memory_Buffers_bytes + node_memory_Cached_bytes +
node_memory_MemFree_bytes) / 1024 / 1024 / 1024`,
`node_memory_MemTotal_bytes / 1024 /1024 / 1024`
],
label: [(d: any) => `${d.id}使用内`, (d: any) => `${d.id}总内存`],
valueFormatter: (value: number) => value.toFixed(2) + "GB",
},
// {
// name: 'Mac cpu',
// query: 'node_cpu_seconds_total',
// label: (d: any) => `cpu: ${d.cpu} mode: ${d.mode}` as string
// },
// {
// name: 'mem',
// query: 'node_memory_free_bytes / 1024 / 1024 / 1024'
// },
// {
// name: 'Mac swap',
// query: 'node_memory_swap_used_bytes / 1024 / 1024 '
// },
])
</script>

Loading…
Cancel
Save