♻️ Merge branch 'feature/server-metric'

This commit is contained in:
ひでまる 2025-01-29 10:15:35 +09:00
commit 1ba7f5daaa
6 changed files with 163 additions and 294 deletions

View file

@ -1,174 +1,74 @@
<!-- <!--
SPDX-FileCopyrightText: syuilo and misskey-project SPDX-FileCopyrightText: syuilo and misskey-project & noridev and misskey-project
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div class="lcfyofjk"> <div :class="$style.root">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> <XPie :class="$style.pie" :value="cpuUsage"/>
<defs> <div :class="$style.div">
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> <p>CPU</p>
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> <text x="50%" y="50%" dy="0.05" text-anchor="middle" :class="$style.text">{{ (cpuUsage * 100).toFixed(1) }}<span style="font-weight: normal; font-size: .75em; margin-left: 2px;">%</span></text>
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> </div>
</linearGradient> <XPie :class="$style.pie" :value="memUsage"/>
<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> <div :class="$style.div">
<polygon <p>RAM</p>
:points="cpuPolygonPoints" <text x="50%" y="50%" dy="0.05" text-anchor="middle" :class="$style.text">{{ (memUsage * 100).toFixed(1) }}<span style="font-weight: normal; font-size: .75em; margin-left: 2px;">%</span></text>
fill="#fff" </div>
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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'; import { onMounted, onBeforeUnmount, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid'; import XPie from './pie-compact.vue';
const props = defineProps<{ const props = defineProps<{
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>, connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse meta: Misskey.entities.ServerInfoResponse
}>(); }>();
const viewBoxX = ref<number>(50); const cpuUsage = ref<number>(0);
const viewBoxY = ref<number>(30); const memUsage = ref<number>(0);
const stats = ref<Misskey.entities.ServerStats[]>([]);
const cpuGradientId = uuid();
const cpuMaskId = uuid();
const memGradientId = uuid();
const memMaskId = uuid();
const cpuPolylinePoints = ref<string>('');
const memPolylinePoints = ref<string>('');
const cpuPolygonPoints = ref<string>('');
const memPolygonPoints = ref<string>('');
const cpuHeadX = ref<number>();
const cpuHeadY = ref<number>();
const memHeadX = ref<number>();
const memHeadY = ref<number>();
const cpuP = ref<string>('');
const memP = ref<string>('');
onMounted(() => { onMounted(() => {
props.connection.on('stats', onStats); props.connection.on('stats', onStats);
props.connection.on('statsLog', onStatsLog);
props.connection.send('requestLog', {
id: Math.random().toString().substring(2, 10),
length: 50,
});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
props.connection.off('stats', onStats); props.connection.off('stats', onStats);
props.connection.off('statsLog', onStatsLog);
}); });
function onStats(connStats: Misskey.entities.ServerStats) { function onStats(stats: Misskey.entities.ServerStats) {
stats.value.push(connStats); cpuUsage.value = stats.cpu;
if (stats.value.length > 50) stats.value.shift(); memUsage.value = stats.mem.active / props.meta.mem.total;
let cpuPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - s.cpu) * viewBoxY.value]);
let memPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY.value]);
cpuPolylinePoints.value = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
memPolylinePoints.value = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
cpuPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${cpuPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
memPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${memPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
cpuHeadX.value = cpuPolylinePointsStats.at(-1)![0];
cpuHeadY.value = cpuPolylinePointsStats.at(-1)![1];
memHeadX.value = memPolylinePointsStats.at(-1)![0];
memHeadY.value = memPolylinePointsStats.at(-1)![1];
cpuP.value = (connStats.cpu * 100).toFixed(0);
memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
}
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
for (const revStats of statsLog.toReversed()) {
onStats(revStats);
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.lcfyofjk { .root {
display: flex; display: flex;
padding: 16px 32px;
}
> svg { .pie {
display: block; height: 42px;
padding: 10px; flex-shrink: 0;
width: 50%; margin-right: 8px;
}
&:first-child { .div {
padding-right: 5px; flex: 1;
} margin: auto;
&:last-child { > p {
padding-left: 5px; margin: 0 0 2px;
} font-size: 0.8em;
> text {
font-size: 4.5px;
fill: currentColor;
> tspan {
opacity: 0.5;
}
}
} }
} }
.text {
font-weight: bold;
fill: currentColor;
}
</style> </style>

View file

@ -11,10 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="meta" data-cy-mkw-serverMetric class="mkw-serverMetric"> <div v-if="meta" data-cy-mkw-serverMetric class="mkw-serverMetric">
<XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/> <XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/> <XCpu v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
<XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/> <XMemory v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
<XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/> <XDisk v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
<XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/>
</div> </div>
</MkContainer> </MkContainer>
</template> </template>
@ -24,7 +23,6 @@ import { onUnmounted, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js';
import XCpuMemory from './cpu-mem.vue'; import XCpuMemory from './cpu-mem.vue';
import XNet from './net.vue';
import XCpu from './cpu.vue'; import XCpu from './cpu.vue';
import XMemory from './mem.vue'; import XMemory from './mem.vue';
import XDisk from './disk.vue'; import XDisk from './disk.vue';
@ -70,7 +68,7 @@ misskeyApiGet('server-info', {}).then(res => {
}); });
const toggleView = () => { const toggleView = () => {
if (widgetProps.view === 4) { if (widgetProps.view === 3) {
widgetProps.view = 0; widgetProps.view = 0;
} else { } else {
widgetProps.view++; widgetProps.view++;

View file

@ -1,147 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<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" setup>
import { onMounted, onBeforeUnmount, ref } from 'vue';
import * as Misskey from 'misskey-js';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
const viewBoxX = ref<number>(50);
const viewBoxY = ref<number>(30);
const stats = ref<Misskey.entities.ServerStats[]>([]);
const inPolylinePoints = ref<string>('');
const outPolylinePoints = ref<string>('');
const inPolygonPoints = ref<string>('');
const outPolygonPoints = ref<string>('');
const inHeadX = ref<number>();
const inHeadY = ref<number>();
const outHeadX = ref<number>();
const outHeadY = ref<number>();
const inRecent = ref<number>(0);
const outRecent = ref<number>(0);
onMounted(() => {
props.connection.on('stats', onStats);
props.connection.on('statsLog', onStatsLog);
props.connection.send('requestLog', {
id: Math.random().toString().substring(2, 10),
length: 50,
});
});
onBeforeUnmount(() => {
props.connection.off('stats', onStats);
props.connection.off('statsLog', onStatsLog);
});
function onStats(connStats: Misskey.entities.ServerStats) {
stats.value.push(connStats);
if (stats.value.length > 50) stats.value.shift();
const inPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.rx)));
const outPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.tx)));
let inPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY.value]);
let outPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY.value]);
inPolylinePoints.value = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
outPolylinePoints.value = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
inPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${inPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
outPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${outPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
inHeadX.value = inPolylinePointsStats.at(-1)![0];
inHeadY.value = inPolylinePointsStats.at(-1)![1];
outHeadX.value = outPolylinePointsStats.at(-1)![0];
outHeadY.value = outPolylinePointsStats.at(-1)![1];
inRecent.value = connStats.net.rx;
outRecent.value = connStats.net.tx;
}
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
for (const revStats of statsLog.toReversed()) {
onStats(revStats);
}
}
</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: 4.5px;
fill: currentColor;
> tspan {
opacity: 0.5;
}
}
}
}
</style>

View file

@ -0,0 +1,54 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<svg :class="$style.root" viewBox="0 0 1 1" preserveAspectRatio="none">
<circle
:r="r"
cx="50%" cy="50%"
fill="none"
stroke-width="0.2"
stroke="rgba(0, 0, 0, 0.05)"
:class="$style.circle"
/>
<circle
:r="r"
cx="50%" cy="50%"
:stroke-dasharray="Math.PI * (r * 2)"
:stroke-dashoffset="strokeDashoffset"
fill="none"
stroke-width="0.2"
:class="$style.circle"
:stroke="color"
/>
</svg>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps<{
value: number;
}>();
const r = 0.35;
const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)));
</script>
<style lang="scss" module>
.root {
display: block;
height: 100%;
}
.circle {
transform-origin: center;
transform: rotate(-90deg);
transition: stroke-dashoffset 0.5s ease;
stroke-linecap: round;
}
</style>

View file

@ -0,0 +1,62 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<svg :class="$style.root" 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)"
:class="$style.circle"
/>
<circle
:r="r"
cx="50%" cy="50%"
:stroke-dasharray="Math.PI * (r * 2)"
:stroke-dashoffset="strokeDashoffset"
fill="none"
stroke-width="0.1"
:class="$style.circle"
:stroke="color"
/>
<text x="50%" y="50%" dy="0.05" text-anchor="middle" :class="$style.text">{{ (value * 100).toFixed(0) }}%</text>
</svg>
</template>
<script lang="ts" setup>
import { computed } from "vue";
const props = defineProps<{
value: number;
}>();
const r = 0.45;
const color = computed(() => `hsl(${180 - props.value * 180}, 80%, 70%)`);
const strokeDashoffset = computed(
() => (1 - props.value) * (Math.PI * (r * 2)),
);
</script>
<style lang="scss" module>
.root {
display: block;
height: 100%;
}
.circle {
transform-origin: center;
transform: rotate(-90deg);
transition: stroke-dashoffset 0.5s ease;
stroke-linecap: round;
}
.text {
font-size: 0.15px;
fill: currentColor;
}
</style>

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:r="r" :r="r"
cx="50%" cy="50%" cx="50%" cy="50%"
fill="none" fill="none"
stroke-width="0.1" stroke-width="0.15"
stroke="rgba(0, 0, 0, 0.05)" stroke="rgba(0, 0, 0, 0.05)"
:class="$style.circle" :class="$style.circle"
/> />
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:stroke-dasharray="Math.PI * (r * 2)" :stroke-dasharray="Math.PI * (r * 2)"
:stroke-dashoffset="strokeDashoffset" :stroke-dashoffset="strokeDashoffset"
fill="none" fill="none"
stroke-width="0.1" stroke-width="0.15"
:class="$style.circle" :class="$style.circle"
:stroke="color" :stroke="color"
/> />
@ -34,7 +34,7 @@ const props = defineProps<{
value: number; value: number;
}>(); }>();
const r = 0.45; const r = 0.4;
const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`); const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2))); const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)));
@ -50,10 +50,12 @@ const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)))
transform-origin: center; transform-origin: center;
transform: rotate(-90deg); transform: rotate(-90deg);
transition: stroke-dashoffset 0.5s ease; transition: stroke-dashoffset 0.5s ease;
stroke-linecap: round;
} }
.text { .text {
font-size: 0.15px; font-size: 0.15px;
fill: currentColor; fill: currentColor;
font-weight: 700;
} }
</style> </style>