サーバー情報ウィジェット
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…
	
	Add table
		Add a link
		
	
		Reference in a new issue