summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/components/drive.file.vue2
-rw-r--r--src/client/widgets/index.ts2
-rw-r--r--src/client/widgets/server-metric/cpu-mem.vue174
-rw-r--r--src/client/widgets/server-metric/cpu.vue78
-rw-r--r--src/client/widgets/server-metric/disk.vue72
-rw-r--r--src/client/widgets/server-metric/index.vue84
-rw-r--r--src/client/widgets/server-metric/mem.vue87
-rw-r--r--src/client/widgets/server-metric/net.vue148
-rw-r--r--src/client/widgets/server-metric/pie.vue65
9 files changed, 711 insertions, 1 deletions
diff --git a/src/client/components/drive.file.vue b/src/client/components/drive.file.vue
index ddee81261e..d2b8b95f73 100644
--- a/src/client/components/drive.file.vue
+++ b/src/client/components/drive.file.vue
@@ -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 copyToClipboard from '@/scripts/copy-to-clipboard';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
-import bytes from '../filters/bytes';
+import bytes from '@/filters/bytes';
import * as os from '@/os';
export default defineComponent({
diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts
index 2c99a73079..8e9232cd90 100644
--- a/src/client/widgets/index.ts
+++ b/src/client/widgets/index.ts
@@ -14,6 +14,7 @@ export default function(app: App) {
app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue')));
app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.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('MkwButton', defineAsyncComponent(() => import('./button.vue')));
}
@@ -32,6 +33,7 @@ export const widgets = [
'federation',
'postForm',
'slideshow',
+ 'serverMetric',
'onlineUsers',
'button',
];
diff --git a/src/client/widgets/server-metric/cpu-mem.vue b/src/client/widgets/server-metric/cpu-mem.vue
new file mode 100644
index 0000000000..f1c31d7cdf
--- /dev/null
+++ b/src/client/widgets/server-metric/cpu-mem.vue
@@ -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>
diff --git a/src/client/widgets/server-metric/cpu.vue b/src/client/widgets/server-metric/cpu.vue
new file mode 100644
index 0000000000..f1574522c2
--- /dev/null
+++ b/src/client/widgets/server-metric/cpu.vue
@@ -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>
diff --git a/src/client/widgets/server-metric/disk.vue b/src/client/widgets/server-metric/disk.vue
new file mode 100644
index 0000000000..41f967d291
--- /dev/null
+++ b/src/client/widgets/server-metric/disk.vue
@@ -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>
diff --git a/src/client/widgets/server-metric/index.vue b/src/client/widgets/server-metric/index.vue
new file mode 100644
index 0000000000..2ce87ba416
--- /dev/null
+++ b/src/client/widgets/server-metric/index.vue
@@ -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>
diff --git a/src/client/widgets/server-metric/mem.vue b/src/client/widgets/server-metric/mem.vue
new file mode 100644
index 0000000000..eab23c1be2
--- /dev/null
+++ b/src/client/widgets/server-metric/mem.vue
@@ -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>
diff --git a/src/client/widgets/server-metric/net.vue b/src/client/widgets/server-metric/net.vue
new file mode 100644
index 0000000000..23c148eeb6
--- /dev/null
+++ b/src/client/widgets/server-metric/net.vue
@@ -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>
diff --git a/src/client/widgets/server-metric/pie.vue b/src/client/widgets/server-metric/pie.vue
new file mode 100644
index 0000000000..38dcf6fcd9
--- /dev/null
+++ b/src/client/widgets/server-metric/pie.vue
@@ -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>