サーバー情報ウィジェット
This commit is contained in:
parent
e74a47916d
commit
c6fe798092
12 changed files with 757 additions and 8 deletions
|
@ -1030,6 +1030,7 @@ _widgets:
|
||||||
slideshow: "スライドショー"
|
slideshow: "スライドショー"
|
||||||
button: "ボタン"
|
button: "ボタン"
|
||||||
onlineUsers: "オンラインユーザー"
|
onlineUsers: "オンラインユーザー"
|
||||||
|
serverMetric: "サーバーメトリクス"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
|
|
@ -36,7 +36,7 @@ import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
|
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
|
||||||
import bytes from '../filters/bytes';
|
import bytes from '@/filters/bytes';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default function(app: App) {
|
||||||
app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue')));
|
app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue')));
|
||||||
app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue')));
|
app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue')));
|
||||||
app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue')));
|
app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue')));
|
||||||
|
app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue')));
|
||||||
app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
|
app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
|
||||||
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
|
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
|
||||||
}
|
}
|
||||||
|
@ -32,6 +33,7 @@ export const widgets = [
|
||||||
'federation',
|
'federation',
|
||||||
'postForm',
|
'postForm',
|
||||||
'slideshow',
|
'slideshow',
|
||||||
|
'serverMetric',
|
||||||
'onlineUsers',
|
'onlineUsers',
|
||||||
'button',
|
'button',
|
||||||
];
|
];
|
||||||
|
|
174
src/client/widgets/server-metric/cpu-mem.vue
Normal file
174
src/client/widgets/server-metric/cpu-mem.vue
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<template>
|
||||||
|
<div class="lcfyofjk">
|
||||||
|
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||||
|
<defs>
|
||||||
|
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
|
||||||
|
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
|
||||||
|
<polygon
|
||||||
|
:points="cpuPolygonPoints"
|
||||||
|
fill="#fff"
|
||||||
|
fill-opacity="0.5"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
:points="cpuPolylinePoints"
|
||||||
|
fill="none"
|
||||||
|
stroke="#fff"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
:cx="cpuHeadX"
|
||||||
|
:cy="cpuHeadY"
|
||||||
|
r="1.5"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
x="-2" y="-2"
|
||||||
|
:width="viewBoxX + 4" :height="viewBoxY + 4"
|
||||||
|
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"
|
||||||
|
/>
|
||||||
|
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
|
||||||
|
</svg>
|
||||||
|
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||||
|
<defs>
|
||||||
|
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
|
||||||
|
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
|
||||||
|
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
|
||||||
|
<polygon
|
||||||
|
:points="memPolygonPoints"
|
||||||
|
fill="#fff"
|
||||||
|
fill-opacity="0.5"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
:points="memPolylinePoints"
|
||||||
|
fill="none"
|
||||||
|
stroke="#fff"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
:cx="memHeadX"
|
||||||
|
:cy="memHeadY"
|
||||||
|
r="1.5"
|
||||||
|
fill="#fff"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
x="-2" y="-2"
|
||||||
|
:width="viewBoxX + 4" :height="viewBoxY + 4"
|
||||||
|
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"
|
||||||
|
/>
|
||||||
|
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
connection: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
viewBoxX: 50,
|
||||||
|
viewBoxY: 30,
|
||||||
|
stats: [],
|
||||||
|
cpuGradientId: uuid(),
|
||||||
|
cpuMaskId: uuid(),
|
||||||
|
memGradientId: uuid(),
|
||||||
|
memMaskId: uuid(),
|
||||||
|
cpuPolylinePoints: '',
|
||||||
|
memPolylinePoints: '',
|
||||||
|
cpuPolygonPoints: '',
|
||||||
|
memPolygonPoints: '',
|
||||||
|
cpuHeadX: null,
|
||||||
|
cpuHeadY: null,
|
||||||
|
memHeadX: null,
|
||||||
|
memHeadY: null,
|
||||||
|
cpuP: '',
|
||||||
|
memP: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.connection.on('stats', this.onStats);
|
||||||
|
this.connection.on('statsLog', this.onStatsLog);
|
||||||
|
this.connection.send('requestLog', {
|
||||||
|
id: Math.random().toString().substr(2, 8)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.connection.off('stats', this.onStats);
|
||||||
|
this.connection.off('statsLog', this.onStatsLog);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onStats(stats) {
|
||||||
|
this.stats.push(stats);
|
||||||
|
if (this.stats.length > 50) this.stats.shift();
|
||||||
|
|
||||||
|
const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu) * this.viewBoxY]);
|
||||||
|
const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / this.meta.mem.total)) * this.viewBoxY]);
|
||||||
|
this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||||
|
this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||||
|
|
||||||
|
this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||||
|
this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||||
|
|
||||||
|
this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
|
||||||
|
this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
|
||||||
|
this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
|
||||||
|
this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
|
||||||
|
|
||||||
|
this.cpuP = (stats.cpu * 100).toFixed(0);
|
||||||
|
this.memP = (stats.mem.used / this.meta.mem.total * 100).toFixed(0);
|
||||||
|
},
|
||||||
|
onStatsLog(statsLog) {
|
||||||
|
for (const stats of [...statsLog].reverse()) {
|
||||||
|
this.onStats(stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.lcfyofjk {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> text {
|
||||||
|
font-size: 5px;
|
||||||
|
fill: currentColor;
|
||||||
|
|
||||||
|
> tspan {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
78
src/client/widgets/server-metric/cpu.vue
Normal file
78
src/client/widgets/server-metric/cpu.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="vrvdvrys">
|
||||||
|
<XPie class="pie" :value="usage"/>
|
||||||
|
<div>
|
||||||
|
<p><fa :icon="faMicrochip"/>CPU</p>
|
||||||
|
<p>{{ meta.cpu.cores }} Logical cores</p>
|
||||||
|
<p>{{ meta.cpu.model }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { faMicrochip } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import XPie from './pie.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
XPie
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
connection: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
usage: 0,
|
||||||
|
faMicrochip,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.connection.on('stats', this.onStats);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.connection.off('stats', this.onStats);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onStats(stats) {
|
||||||
|
this.usage = stats.cpu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vrvdvrys {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
> .pie {
|
||||||
|
height: 82px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
> [data-icon] {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
72
src/client/widgets/server-metric/disk.vue
Normal file
72
src/client/widgets/server-metric/disk.vue
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<template>
|
||||||
|
<div class="zbwaqsat">
|
||||||
|
<XPie class="pie" :value="usage"/>
|
||||||
|
<div>
|
||||||
|
<p><fa :icon="faHdd"/>Disk</p>
|
||||||
|
<p>Total: {{ bytes(total, 1) }}</p>
|
||||||
|
<p>Free: {{ bytes(available, 1) }}</p>
|
||||||
|
<p>Used: {{ bytes(used, 1) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { faHdd } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import XPie from './pie.vue';
|
||||||
|
import bytes from '@/filters/bytes';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
XPie
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
meta: {
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
usage: this.meta.fs.used / this.meta.fs.total,
|
||||||
|
total: this.meta.fs.total,
|
||||||
|
used: this.meta.fs.used,
|
||||||
|
available: this.meta.fs.total - this.meta.fs.used,
|
||||||
|
faHdd,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.zbwaqsat {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
> .pie {
|
||||||
|
height: 82px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
> [data-icon] {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
84
src/client/widgets/server-metric/index.vue
Normal file
84
src/client/widgets/server-metric/index.vue
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<MkContainer :show-header="props.showHeader" :naked="props.transparent">
|
||||||
|
<template #header><Fa :icon="faServer"/>{{ $ts._widgets.serverMetric }}</template>
|
||||||
|
<template #func><button @click="toggleView()" class="_button"><Fa :icon="faSort"/></button></template>
|
||||||
|
|
||||||
|
<div class="mkw-serverMetric" v-if="meta">
|
||||||
|
<XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/>
|
||||||
|
<XNet v-if="props.view === 1" :connection="connection" :meta="meta"/>
|
||||||
|
<XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/>
|
||||||
|
<XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/>
|
||||||
|
<XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/>
|
||||||
|
</div>
|
||||||
|
</MkContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { faServer, faSort } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import define from '../define';
|
||||||
|
import MkContainer from '@/components/ui/container.vue';
|
||||||
|
import XCpuMemory from './cpu-mem.vue';
|
||||||
|
import XNet from './net.vue';
|
||||||
|
import XCpu from './cpu.vue';
|
||||||
|
import XMemory from './mem.vue';
|
||||||
|
import XDisk from './disk.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
const widget = define({
|
||||||
|
name: 'serverMetric',
|
||||||
|
props: () => ({
|
||||||
|
showHeader: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
transparent: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
extends: widget,
|
||||||
|
components: {
|
||||||
|
MkContainer,
|
||||||
|
XCpuMemory,
|
||||||
|
XNet,
|
||||||
|
XCpu,
|
||||||
|
XMemory,
|
||||||
|
XDisk,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
meta: null,
|
||||||
|
connection: null,
|
||||||
|
faServer, faSort,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
os.api('server-info', {}).then(res => {
|
||||||
|
this.meta = res;
|
||||||
|
});
|
||||||
|
this.connection = os.stream.useSharedConnection('serverStats');
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.connection.dispose();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleView() {
|
||||||
|
if (this.props.view == 4) {
|
||||||
|
this.props.view = 0;
|
||||||
|
} else {
|
||||||
|
this.props.view++;
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
87
src/client/widgets/server-metric/mem.vue
Normal file
87
src/client/widgets/server-metric/mem.vue
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<div class="zlxnikvl">
|
||||||
|
<XPie class="pie" :value="usage"/>
|
||||||
|
<div>
|
||||||
|
<p><fa :icon="faMemory"/>RAM</p>
|
||||||
|
<p>Total: {{ bytes(total, 1) }}</p>
|
||||||
|
<p>Used: {{ bytes(used, 1) }}</p>
|
||||||
|
<p>Free: {{ bytes(free, 1) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { faMemory } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import XPie from './pie.vue';
|
||||||
|
import bytes from '@/filters/bytes';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
XPie
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
connection: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
usage: 0,
|
||||||
|
total: 0,
|
||||||
|
used: 0,
|
||||||
|
free: 0,
|
||||||
|
faMemory,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.connection.on('stats', this.onStats);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.connection.off('stats', this.onStats);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onStats(stats) {
|
||||||
|
this.usage = stats.mem.used / this.meta.mem.total;
|
||||||
|
this.total = this.meta.mem.total;
|
||||||
|
this.used = stats.mem.used;
|
||||||
|
this.free = this.meta.mem.total - stats.mem.used;
|
||||||
|
},
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.zlxnikvl {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
> .pie {
|
||||||
|
height: 82px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
> [data-icon] {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
148
src/client/widgets/server-metric/net.vue
Normal file
148
src/client/widgets/server-metric/net.vue
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
<template>
|
||||||
|
<div class="oxxrhrto">
|
||||||
|
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||||
|
<polygon
|
||||||
|
:points="inPolygonPoints"
|
||||||
|
fill="#94a029"
|
||||||
|
fill-opacity="0.5"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
:points="inPolylinePoints"
|
||||||
|
fill="none"
|
||||||
|
stroke="#94a029"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
:cx="inHeadX"
|
||||||
|
:cy="inHeadY"
|
||||||
|
r="1.5"
|
||||||
|
fill="#94a029"
|
||||||
|
/>
|
||||||
|
<text x="1" y="5">NET rx <tspan>{{ bytes(inRecent) }}</tspan></text>
|
||||||
|
</svg>
|
||||||
|
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||||
|
<polygon
|
||||||
|
:points="outPolygonPoints"
|
||||||
|
fill="#ff9156"
|
||||||
|
fill-opacity="0.5"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
:points="outPolylinePoints"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ff9156"
|
||||||
|
stroke-width="1"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
:cx="outHeadX"
|
||||||
|
:cy="outHeadY"
|
||||||
|
r="1.5"
|
||||||
|
fill="#ff9156"
|
||||||
|
/>
|
||||||
|
<text x="1" y="5">NET tx <tspan>{{ bytes(outRecent) }}</tspan></text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import bytes from '@/filters/bytes';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
connection: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
viewBoxX: 50,
|
||||||
|
viewBoxY: 30,
|
||||||
|
stats: [],
|
||||||
|
inPolylinePoints: '',
|
||||||
|
outPolylinePoints: '',
|
||||||
|
inPolygonPoints: '',
|
||||||
|
outPolygonPoints: '',
|
||||||
|
inHeadX: null,
|
||||||
|
inHeadY: null,
|
||||||
|
outHeadX: null,
|
||||||
|
outHeadY: null,
|
||||||
|
inRecent: 0,
|
||||||
|
outRecent: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.connection.on('stats', this.onStats);
|
||||||
|
this.connection.on('statsLog', this.onStatsLog);
|
||||||
|
this.connection.send('requestLog', {
|
||||||
|
id: Math.random().toString().substr(2, 8)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.connection.off('stats', this.onStats);
|
||||||
|
this.connection.off('statsLog', this.onStatsLog);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onStats(stats) {
|
||||||
|
this.stats.push(stats);
|
||||||
|
if (this.stats.length > 50) this.stats.shift();
|
||||||
|
|
||||||
|
const inPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.rx)));
|
||||||
|
const outPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.tx)));
|
||||||
|
|
||||||
|
const inPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * this.viewBoxY]);
|
||||||
|
const outPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * this.viewBoxY]);
|
||||||
|
this.inPolylinePoints = inPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||||
|
this.outPolylinePoints = outPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||||
|
|
||||||
|
this.inPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.inPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||||
|
this.outPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.outPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||||
|
|
||||||
|
this.inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0];
|
||||||
|
this.inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1];
|
||||||
|
this.outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0];
|
||||||
|
this.outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1];
|
||||||
|
|
||||||
|
this.inRecent = stats.net.rx;
|
||||||
|
this.outRecent = stats.net.tx;
|
||||||
|
},
|
||||||
|
onStatsLog(statsLog) {
|
||||||
|
for (const stats of [...statsLog].reverse()) {
|
||||||
|
this.onStats(stats);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.oxxrhrto {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> text {
|
||||||
|
font-size: 5px;
|
||||||
|
fill: currentColor;
|
||||||
|
|
||||||
|
> tspan {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
65
src/client/widgets/server-metric/pie.vue
Normal file
65
src/client/widgets/server-metric/pie.vue
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none">
|
||||||
|
<circle
|
||||||
|
:r="r"
|
||||||
|
cx="50%" cy="50%"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="0.1"
|
||||||
|
stroke="rgba(0, 0, 0, 0.05)"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
:r="r"
|
||||||
|
cx="50%" cy="50%"
|
||||||
|
:stroke-dasharray="Math.PI * (r * 2)"
|
||||||
|
:stroke-dashoffset="strokeDashoffset"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="0.1"
|
||||||
|
:stroke="color"
|
||||||
|
/>
|
||||||
|
<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
r: 0.45
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
color(): string {
|
||||||
|
return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
|
||||||
|
},
|
||||||
|
strokeDashoffset(): number {
|
||||||
|
return (1 - this.value) * (Math.PI * (this.r * 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hsalcinq {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> circle {
|
||||||
|
transform-origin: center;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: stroke-dashoffset 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
> text {
|
||||||
|
font-size: 0.15px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,6 +6,9 @@ const ev = new Xev();
|
||||||
|
|
||||||
const interval = 2000;
|
const interval = 2000;
|
||||||
|
|
||||||
|
const roundCpu = (num: number) => Math.round(num * 1000) / 1000;
|
||||||
|
const round = (num: number) => Math.round(num * 10) / 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report server stats regularly
|
* Report server stats regularly
|
||||||
*/
|
*/
|
||||||
|
@ -23,18 +26,18 @@ export default function() {
|
||||||
const fsStats = await fs();
|
const fsStats = await fs();
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
cpu: cpu,
|
cpu: roundCpu(cpu),
|
||||||
mem: {
|
mem: {
|
||||||
used: memStats.used,
|
used: round(memStats.used),
|
||||||
active: memStats.active,
|
active: round(memStats.active),
|
||||||
},
|
},
|
||||||
net: {
|
net: {
|
||||||
rx: Math.max(0, netStats.rx_sec),
|
rx: round(Math.max(0, netStats.rx_sec)),
|
||||||
tx: Math.max(0, netStats.tx_sec),
|
tx: round(Math.max(0, netStats.tx_sec)),
|
||||||
},
|
},
|
||||||
fs: {
|
fs: {
|
||||||
r: Math.max(0, fsStats.rIO_sec),
|
r: round(Math.max(0, fsStats.rIO_sec)),
|
||||||
w: Math.max(0, fsStats.wIO_sec),
|
w: round(Math.max(0, fsStats.wIO_sec)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ev.emit('serverStats', stats);
|
ev.emit('serverStats', stats);
|
||||||
|
|
35
src/server/api/endpoints/server-info.ts
Normal file
35
src/server/api/endpoints/server-info.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as si from 'systeminformation';
|
||||||
|
import define from '../define';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: false as const,
|
||||||
|
|
||||||
|
desc: {
|
||||||
|
},
|
||||||
|
|
||||||
|
tags: ['meta'],
|
||||||
|
|
||||||
|
params: {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, async () => {
|
||||||
|
const memStats = await si.mem();
|
||||||
|
const fsStats = await si.fsSize();
|
||||||
|
|
||||||
|
return {
|
||||||
|
machine: os.hostname(),
|
||||||
|
cpu: {
|
||||||
|
model: os.cpus()[0].model,
|
||||||
|
cores: os.cpus().length
|
||||||
|
},
|
||||||
|
mem: {
|
||||||
|
total: memStats.total
|
||||||
|
},
|
||||||
|
fs: {
|
||||||
|
total: fsStats[0].size,
|
||||||
|
used: fsStats[0].used,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
Loading…
Reference in a new issue