enhance(client): improve control panel
This commit is contained in:
parent
c67c0df762
commit
0248a2a989
|
@ -1,232 +0,0 @@
|
||||||
<template>
|
|
||||||
<canvas ref="chartEl"></canvas>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import {
|
|
||||||
Chart,
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
} from 'chart.js';
|
|
||||||
import number from '@/filters/number';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { defaultStore } from '@/store';
|
|
||||||
|
|
||||||
Chart.register(
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
);
|
|
||||||
|
|
||||||
const alpha = (hex, a) => {
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
|
||||||
const r = parseInt(result[1], 16);
|
|
||||||
const g = parseInt(result[2], 16);
|
|
||||||
const b = parseInt(result[3], 16);
|
|
||||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
domain: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
connection: {
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
const chartEl = ref<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
||||||
|
|
||||||
// フォントカラー
|
|
||||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const chartInstance = new Chart(chartEl.value, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: [],
|
|
||||||
datasets: [{
|
|
||||||
label: 'Process',
|
|
||||||
pointRadius: 0,
|
|
||||||
tension: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderColor: '#00E396',
|
|
||||||
backgroundColor: alpha('#00E396', 0.1),
|
|
||||||
data: []
|
|
||||||
}, {
|
|
||||||
label: 'Active',
|
|
||||||
pointRadius: 0,
|
|
||||||
tension: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderColor: '#00BCD4',
|
|
||||||
backgroundColor: alpha('#00BCD4', 0.1),
|
|
||||||
data: []
|
|
||||||
}, {
|
|
||||||
label: 'Waiting',
|
|
||||||
pointRadius: 0,
|
|
||||||
tension: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderColor: '#FFB300',
|
|
||||||
backgroundColor: alpha('#FFB300', 0.1),
|
|
||||||
yAxisID: 'y2',
|
|
||||||
data: []
|
|
||||||
}, {
|
|
||||||
label: 'Delayed',
|
|
||||||
pointRadius: 0,
|
|
||||||
tension: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderColor: '#E53935',
|
|
||||||
borderDash: [5, 5],
|
|
||||||
fill: false,
|
|
||||||
yAxisID: 'y2',
|
|
||||||
data: []
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 16,
|
|
||||||
bottom: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
grid: {
|
|
||||||
display: true,
|
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
display: false,
|
|
||||||
maxTicksLimit: 10
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
min: 0,
|
|
||||||
stack: 'queue',
|
|
||||||
stackWeight: 2,
|
|
||||||
grid: {
|
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y2: {
|
|
||||||
min: 0,
|
|
||||||
offset: true,
|
|
||||||
stack: 'queue',
|
|
||||||
stackWeight: 1,
|
|
||||||
grid: {
|
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
interaction: {
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'bottom',
|
|
||||||
labels: {
|
|
||||||
boxWidth: 16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: 'index',
|
|
||||||
animation: {
|
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onStats = (stats) => {
|
|
||||||
chartInstance.data.labels.push('');
|
|
||||||
chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
|
|
||||||
chartInstance.data.datasets[1].data.push(stats[props.domain].active);
|
|
||||||
chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
|
|
||||||
chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
|
|
||||||
if (chartInstance.data.datasets[0].data.length > 200) {
|
|
||||||
chartInstance.data.labels.shift();
|
|
||||||
chartInstance.data.datasets[0].data.shift();
|
|
||||||
chartInstance.data.datasets[1].data.shift();
|
|
||||||
chartInstance.data.datasets[2].data.shift();
|
|
||||||
chartInstance.data.datasets[3].data.shift();
|
|
||||||
}
|
|
||||||
chartInstance.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStatsLog = (statsLog) => {
|
|
||||||
for (const stats of [...statsLog].reverse()) {
|
|
||||||
chartInstance.data.labels.push('');
|
|
||||||
chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
|
|
||||||
chartInstance.data.datasets[1].data.push(stats[props.domain].active);
|
|
||||||
chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
|
|
||||||
chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
|
|
||||||
if (chartInstance.data.datasets[0].data.length > 200) {
|
|
||||||
chartInstance.data.labels.shift();
|
|
||||||
chartInstance.data.datasets[0].data.shift();
|
|
||||||
chartInstance.data.datasets[1].data.shift();
|
|
||||||
chartInstance.data.datasets[2].data.shift();
|
|
||||||
chartInstance.data.datasets[3].data.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chartInstance.update();
|
|
||||||
};
|
|
||||||
|
|
||||||
props.connection.on('stats', onStats);
|
|
||||||
props.connection.on('statsLog', onStatsLog);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
props.connection.off('stats', onStats);
|
|
||||||
props.connection.off('statsLog', onStatsLog);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
chartEl,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<div class="wbrkwale">
|
||||||
|
<MkLoading v-if="fetching"/>
|
||||||
|
<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
|
||||||
|
<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
|
||||||
|
<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
|
||||||
|
<div class="body">
|
||||||
|
<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.name ?? instance.host }}</a>
|
||||||
|
<p>{{ instance.host }}</p>
|
||||||
|
</div>
|
||||||
|
<MkMiniChart class="chart" :src="charts[i].requests.received"/>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import MkMiniChart from '@/components/mini-chart.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
const instances = ref([]);
|
||||||
|
const charts = ref([]);
|
||||||
|
const fetching = ref(true);
|
||||||
|
|
||||||
|
const fetch = async () => {
|
||||||
|
const fetchedInstances = await os.api('federation/instances', {
|
||||||
|
sort: '+lastCommunicatedAt',
|
||||||
|
limit: 5,
|
||||||
|
});
|
||||||
|
const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.apiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
|
||||||
|
instances.value = fetchedInstances;
|
||||||
|
charts.value = fetchedCharts;
|
||||||
|
fetching.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let intervalId;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetch();
|
||||||
|
intervalId = window.setInterval(fetch, 1000 * 60);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wbrkwale {
|
||||||
|
> .instances {
|
||||||
|
.chart-move {
|
||||||
|
transition: transform 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .instance {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> img {
|
||||||
|
display: block;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--fg);
|
||||||
|
padding-right: 8px;
|
||||||
|
|
||||||
|
> .a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 75%;
|
||||||
|
opacity: 0.7;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .chart {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,213 @@
|
||||||
|
<template>
|
||||||
|
<canvas ref="chartEl"></canvas>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
} from 'chart.js';
|
||||||
|
import number from '@/filters/number';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
domain: string;
|
||||||
|
connection: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const alpha = (hex, a) => {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||||
|
const r = parseInt(result[1], 16);
|
||||||
|
const g = parseInt(result[2], 16);
|
||||||
|
const b = parseInt(result[3], 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartEl = ref<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
|
// フォントカラー
|
||||||
|
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||||
|
|
||||||
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
|
let chartInstance: Chart;
|
||||||
|
|
||||||
|
const onStats = (stats) => {
|
||||||
|
chartInstance.data.labels.push('');
|
||||||
|
chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
|
||||||
|
chartInstance.data.datasets[1].data.push(stats[props.domain].active);
|
||||||
|
chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
|
||||||
|
chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
|
||||||
|
if (chartInstance.data.datasets[0].data.length > 200) {
|
||||||
|
chartInstance.data.labels.shift();
|
||||||
|
chartInstance.data.datasets[0].data.shift();
|
||||||
|
chartInstance.data.datasets[1].data.shift();
|
||||||
|
chartInstance.data.datasets[2].data.shift();
|
||||||
|
chartInstance.data.datasets[3].data.shift();
|
||||||
|
}
|
||||||
|
chartInstance.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStatsLog = (statsLog) => {
|
||||||
|
for (const stats of [...statsLog].reverse()) {
|
||||||
|
chartInstance.data.labels.push('');
|
||||||
|
chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
|
||||||
|
chartInstance.data.datasets[1].data.push(stats[props.domain].active);
|
||||||
|
chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
|
||||||
|
chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
|
||||||
|
if (chartInstance.data.datasets[0].data.length > 200) {
|
||||||
|
chartInstance.data.labels.shift();
|
||||||
|
chartInstance.data.datasets[0].data.shift();
|
||||||
|
chartInstance.data.datasets[1].data.shift();
|
||||||
|
chartInstance.data.datasets[2].data.shift();
|
||||||
|
chartInstance.data.datasets[3].data.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chartInstance.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
chartInstance = new Chart(chartEl.value, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Process',
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.3,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderColor: '#00E396',
|
||||||
|
backgroundColor: alpha('#00E396', 0.1),
|
||||||
|
data: [],
|
||||||
|
}, {
|
||||||
|
label: 'Active',
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.3,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderColor: '#00BCD4',
|
||||||
|
backgroundColor: alpha('#00BCD4', 0.1),
|
||||||
|
data: [],
|
||||||
|
}, {
|
||||||
|
label: 'Waiting',
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.3,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderColor: '#FFB300',
|
||||||
|
backgroundColor: alpha('#FFB300', 0.1),
|
||||||
|
data: [],
|
||||||
|
}, {
|
||||||
|
label: 'Delayed',
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.3,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderColor: '#E53935',
|
||||||
|
borderDash: [5, 5],
|
||||||
|
fill: false,
|
||||||
|
data: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
aspectRatio: 2.5,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 0,
|
||||||
|
right: 8,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: false,
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'index',
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
external: externalTooltipHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
props.connection.on('stats', onStats);
|
||||||
|
props.connection.on('statsLog', onStatsLog);
|
||||||
|
|
||||||
|
props.connection.send('requestLog', {
|
||||||
|
id: Math.random().toString().substr(2, 8),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
props.connection.off('stats', onStats);
|
||||||
|
props.connection.off('statsLog', onStatsLog);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,92 +1,318 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-size="{ max: [740] }" class="edbbcaef">
|
<MkSpacer :content-max="900">
|
||||||
<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
|
<div ref="rootEl" v-size="{ max: [740] }" class="edbbcaef">
|
||||||
<div class="number _panel">
|
<div class="left">
|
||||||
<div class="label">Users</div>
|
<div v-if="stats" class="container stats">
|
||||||
<div class="value _monospace">
|
<div class="title">Stats</div>
|
||||||
{{ number(stats.originalUsersCount) }}
|
<div class="body">
|
||||||
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
<div class="number _panel">
|
||||||
|
<div class="label">Users</div>
|
||||||
|
<div class="value _monospace">
|
||||||
|
{{ number(stats.originalUsersCount) }}
|
||||||
|
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="number _panel">
|
||||||
|
<div class="label">Notes</div>
|
||||||
|
<div class="value _monospace">
|
||||||
|
{{ number(stats.originalNotesCount) }}
|
||||||
|
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container queue">
|
||||||
|
<div class="title">Job queue</div>
|
||||||
|
<div class="body deliver">
|
||||||
|
<div class="title">Deliver</div>
|
||||||
|
<XQueueChart :connection="queueStatsConnection" domain="deliver"/>
|
||||||
|
</div>
|
||||||
|
<div class="body inbox">
|
||||||
|
<div class="title">Inbox</div>
|
||||||
|
<XQueueChart :connection="queueStatsConnection" domain="inbox"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--<XMetrics/>-->
|
||||||
|
|
||||||
|
<div class="container env">
|
||||||
|
<div class="title">Enviroment</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="number _panel">
|
||||||
|
<div class="label">Misskey</div>
|
||||||
|
<div class="value _monospace">{{ version }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="serverInfo" class="number _panel">
|
||||||
|
<div class="label">Node.js</div>
|
||||||
|
<div class="value _monospace">{{ serverInfo.node }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="serverInfo" class="number _panel">
|
||||||
|
<div class="label">PostgreSQL</div>
|
||||||
|
<div class="value _monospace">{{ serverInfo.psql }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="serverInfo" class="number _panel">
|
||||||
|
<div class="label">Redis</div>
|
||||||
|
<div class="value _monospace">{{ serverInfo.redis }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="number _panel">
|
||||||
|
<div class="label">Vue</div>
|
||||||
|
<div class="value _monospace">{{ vueVersion }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="number _panel">
|
<div class="right">
|
||||||
<div class="label">Notes</div>
|
<div class="container charts">
|
||||||
<div class="value _monospace">
|
<div class="title">Active users</div>
|
||||||
{{ number(stats.originalNotesCount) }}
|
<div class="body">
|
||||||
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
<canvas ref="chartEl"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container federation">
|
||||||
|
<div class="title">Active instances</div>
|
||||||
|
<div class="body">
|
||||||
|
<XFederation/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
<MkContainer :foldable="true" class="charts">
|
|
||||||
<template #header><i class="fas fa-chart-bar"></i>{{ i18n.ts.charts }}</template>
|
|
||||||
<div style="padding: 12px;">
|
|
||||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
|
||||||
</div>
|
|
||||||
</MkContainer>
|
|
||||||
|
|
||||||
<div class="queue">
|
|
||||||
<MkContainer :foldable="true" :thin="true" class="deliver">
|
|
||||||
<template #header>Queue: deliver</template>
|
|
||||||
<MkQueueChart :connection="queueStatsConnection" domain="deliver"/>
|
|
||||||
</MkContainer>
|
|
||||||
<MkContainer :foldable="true" :thin="true" class="inbox">
|
|
||||||
<template #header>Queue: inbox</template>
|
|
||||||
<MkQueueChart :connection="queueStatsConnection" domain="inbox"/>
|
|
||||||
</MkContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--<XMetrics/>-->
|
|
||||||
|
|
||||||
<MkFolder style="margin: var(--margin)">
|
|
||||||
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
|
|
||||||
<div class="cfcdecdf">
|
|
||||||
<div class="number _panel">
|
|
||||||
<div class="label">Misskey</div>
|
|
||||||
<div class="value _monospace">{{ version }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="serverInfo" class="number _panel">
|
|
||||||
<div class="label">Node.js</div>
|
|
||||||
<div class="value _monospace">{{ serverInfo.node }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="serverInfo" class="number _panel">
|
|
||||||
<div class="label">PostgreSQL</div>
|
|
||||||
<div class="value _monospace">{{ serverInfo.psql }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="serverInfo" class="number _panel">
|
|
||||||
<div class="label">Redis</div>
|
|
||||||
<div class="value _monospace">{{ serverInfo.redis }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="number _panel">
|
|
||||||
<div class="label">Vue</div>
|
|
||||||
<div class="value _monospace">{{ vueVersion }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MkFolder>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
} from 'chart.js';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import MagicGrid from 'magic-grid';
|
||||||
import XMetrics from './metrics.vue';
|
import XMetrics from './metrics.vue';
|
||||||
|
import XFederation from './overview.federation.vue';
|
||||||
|
import XQueueChart from './overview.queue-chart.vue';
|
||||||
import MkInstanceStats from '@/components/instance-stats.vue';
|
import MkInstanceStats from '@/components/instance-stats.vue';
|
||||||
import MkNumberDiff from '@/components/number-diff.vue';
|
import MkNumberDiff from '@/components/number-diff.vue';
|
||||||
import MkContainer from '@/components/ui/container.vue';
|
|
||||||
import MkFolder from '@/components/ui/folder.vue';
|
|
||||||
import MkQueueChart from '@/components/queue-chart.vue';
|
|
||||||
import { version, url } from '@/config';
|
import { version, url } from '@/config';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
//gradient,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rootEl = $ref<HTMLElement>();
|
||||||
|
const chartEl = $ref<HTMLCanvasElement>(null);
|
||||||
let stats: any = $ref(null);
|
let stats: any = $ref(null);
|
||||||
let serverInfo: any = $ref(null);
|
let serverInfo: any = $ref(null);
|
||||||
let usersComparedToThePrevDay: any = $ref(null);
|
let usersComparedToThePrevDay: any = $ref(null);
|
||||||
let notesComparedToThePrevDay: any = $ref(null);
|
let notesComparedToThePrevDay: any = $ref(null);
|
||||||
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
|
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
|
||||||
|
const now = new Date();
|
||||||
|
let chartInstance: Chart = null;
|
||||||
|
const chartLimit = 30;
|
||||||
|
|
||||||
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
|
async function renderChart() {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDate = (ago: number) => {
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = now.getMonth();
|
||||||
|
const d = now.getDate();
|
||||||
|
|
||||||
|
return new Date(y, m, d - ago);
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (arr) => {
|
||||||
|
return arr.map((v, i) => ({
|
||||||
|
x: getDate(i).getTime(),
|
||||||
|
y: v,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
|
||||||
|
|
||||||
|
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||||
|
|
||||||
|
// フォントカラー
|
||||||
|
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||||
|
|
||||||
|
const color = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
||||||
|
|
||||||
|
chartInstance = new Chart(chartEl, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
//labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
|
||||||
|
datasets: [{
|
||||||
|
parsing: false,
|
||||||
|
label: 'a',
|
||||||
|
data: format(raw.readWrite).slice().reverse(),
|
||||||
|
tension: 0.3,
|
||||||
|
pointRadius: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: color,
|
||||||
|
/*gradient: props.bar ? undefined : {
|
||||||
|
backgroundColor: {
|
||||||
|
axis: 'y',
|
||||||
|
colors: {
|
||||||
|
0: alpha(x.color ? x.color : getColor(i), 0),
|
||||||
|
[maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},*/
|
||||||
|
barPercentage: 0.9,
|
||||||
|
categoryPercentage: 0.9,
|
||||||
|
clip: 8,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
aspectRatio: 2.5,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 0,
|
||||||
|
right: 8,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
stacked: true,
|
||||||
|
offset: false,
|
||||||
|
time: {
|
||||||
|
stepSize: 1,
|
||||||
|
unit: 'month',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
locale: enUS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
min: getDate(chartLimit).getTime(),
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
position: 'left',
|
||||||
|
stacked: true,
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: false,
|
||||||
|
//mirror: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index',
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
hoverRadius: 5,
|
||||||
|
hoverBorderWidth: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'index',
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
external: externalTooltipHandler,
|
||||||
|
},
|
||||||
|
//gradient,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [{
|
||||||
|
id: 'vLine',
|
||||||
|
beforeDraw(chart, args, options) {
|
||||||
|
if (chart.tooltip._active && chart.tooltip._active.length) {
|
||||||
|
const activePoint = chart.tooltip._active[0];
|
||||||
|
const ctx = chart.ctx;
|
||||||
|
const x = activePoint.element.x;
|
||||||
|
const topY = chart.scales.y.top;
|
||||||
|
const bottomY = chart.scales.y.bottom;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, bottomY);
|
||||||
|
ctx.lineTo(x, topY);
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeStyle = vLineColor;
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
/*
|
||||||
|
const magicGrid = new MagicGrid({
|
||||||
|
container: rootEl,
|
||||||
|
static: true,
|
||||||
|
animate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
magicGrid.listen();
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderChart();
|
||||||
|
|
||||||
os.api('stats', {}).then(statsResponse => {
|
os.api('stats', {}).then(statsResponse => {
|
||||||
stats = statsResponse;
|
stats = statsResponse;
|
||||||
|
|
||||||
|
@ -128,63 +354,108 @@ definePageMetadata({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.edbbcaef {
|
.edbbcaef {
|
||||||
.cfcdecdf {
|
display: flex;
|
||||||
display: grid;
|
|
||||||
grid-gap: 8px;
|
|
||||||
grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
|
|
||||||
|
|
||||||
> .number {
|
> .left, > .right {
|
||||||
padding: 12px 16px;
|
box-sizing: border-box;
|
||||||
|
width: 50%;
|
||||||
|
|
||||||
> .label {
|
> .container {
|
||||||
opacity: 0.7;
|
margin: 32px 0;
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .value {
|
> .title {
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
> .diff {
|
&.stats {
|
||||||
font-size: 0.8em;
|
> .body {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
|
||||||
|
> .number {
|
||||||
|
padding: 14px 20px;
|
||||||
|
|
||||||
|
> .label {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .value {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5em;
|
||||||
|
|
||||||
|
> .diff {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.env {
|
||||||
|
> .body {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
|
||||||
|
> .number {
|
||||||
|
padding: 14px 20px;
|
||||||
|
|
||||||
|
> .label {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .value {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.charts {
|
||||||
|
> .body {
|
||||||
|
padding: 32px;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.federation {
|
||||||
|
> .body {
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.queue {
|
||||||
|
> .body {
|
||||||
|
padding: 32px;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .charts {
|
> .left {
|
||||||
margin: var(--margin);
|
padding-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .queue {
|
> .right {
|
||||||
margin: var(--margin);
|
padding-left: 16px;
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> .deliver,
|
|
||||||
> .inbox {
|
|
||||||
flex: 1;
|
|
||||||
width: 50%;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-left: var(--margin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width_740px {
|
|
||||||
> .queue {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
> .deliver,
|
|
||||||
> .inbox {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
margin-top: var(--margin);
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
<template>
|
||||||
|
<canvas ref="chartEl"></canvas>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { watch, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
} from 'chart.js';
|
||||||
|
import number from '@/filters/number';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const alpha = (hex, a) => {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||||
|
const r = parseInt(result[1], 16);
|
||||||
|
const g = parseInt(result[2], 16);
|
||||||
|
const b = parseInt(result[3], 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartEl = ref<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
|
// フォントカラー
|
||||||
|
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||||
|
|
||||||
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
|
let chartInstance: Chart;
|
||||||
|
|
||||||
|
function setData(values) {
|
||||||
|
if (chartInstance == null) return;
|
||||||
|
for (const value of values) {
|
||||||
|
chartInstance.data.labels.push('');
|
||||||
|
chartInstance.data.datasets[0].data.push(value);
|
||||||
|
if (chartInstance.data.datasets[0].data.length > 200) {
|
||||||
|
chartInstance.data.labels.shift();
|
||||||
|
chartInstance.data.datasets[0].data.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chartInstance.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushData(value) {
|
||||||
|
if (chartInstance == null) return;
|
||||||
|
chartInstance.data.labels.push('');
|
||||||
|
chartInstance.data.datasets[0].data.push(value);
|
||||||
|
if (chartInstance.data.datasets[0].data.length > 200) {
|
||||||
|
chartInstance.data.labels.shift();
|
||||||
|
chartInstance.data.datasets[0].data.shift();
|
||||||
|
}
|
||||||
|
chartInstance.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
const label =
|
||||||
|
props.type === 'process' ? 'Process' :
|
||||||
|
props.type === 'active' ? 'Active' :
|
||||||
|
props.type === 'delayed' ? 'Delayed' :
|
||||||
|
props.type === 'waiting' ? 'Waiting' :
|
||||||
|
'?' as never;
|
||||||
|
|
||||||
|
const color =
|
||||||
|
props.type === 'process' ? '#00E396' :
|
||||||
|
props.type === 'active' ? '#00BCD4' :
|
||||||
|
props.type === 'delayed' ? '#E53935' :
|
||||||
|
props.type === 'waiting' ? '#FFB300' :
|
||||||
|
'?' as never;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
chartInstance = new Chart(chartEl.value, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [{
|
||||||
|
label: label,
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.3,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
borderColor: color,
|
||||||
|
backgroundColor: alpha(color, 0.1),
|
||||||
|
data: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
aspectRatio: 2.5,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 0,
|
||||||
|
right: 8,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: gridColor,
|
||||||
|
borderColor: 'rgb(0, 0, 0, 0)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: false,
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: 0,
|
||||||
|
grid: {
|
||||||
|
color: gridColor,
|
||||||
|
borderColor: 'rgb(0, 0, 0, 0)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'index',
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
external: externalTooltipHandler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setData,
|
||||||
|
pushData,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,80 +1,148 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="_debobigegoItem">
|
<div class="pumxzjhg">
|
||||||
<div class="_debobigegoLabel"><slot name="title"></slot></div>
|
<div class="_table status">
|
||||||
<div class="_debobigegoPanel pumxzjhg">
|
<div class="_row">
|
||||||
<div class="_table status">
|
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
||||||
<div class="_row">
|
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
||||||
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
|
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
|
||||||
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
|
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
||||||
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
|
</div>
|
||||||
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
|
</div>
|
||||||
|
<div class="charts">
|
||||||
|
<div class="chart">
|
||||||
|
<div class="title">Process</div>
|
||||||
|
<XChart ref="chartProcess" type="process"/>
|
||||||
|
</div>
|
||||||
|
<div class="chart">
|
||||||
|
<div class="title">Active</div>
|
||||||
|
<XChart ref="chartActive" type="active"/>
|
||||||
|
</div>
|
||||||
|
<div class="chart">
|
||||||
|
<div class="title">Delayed</div>
|
||||||
|
<XChart ref="chartDelayed" type="delayed"/>
|
||||||
|
</div>
|
||||||
|
<div class="chart">
|
||||||
|
<div class="title">Waiting</div>
|
||||||
|
<XChart ref="chartWaiting" type="waiting"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="jobs">
|
||||||
|
<div v-if="jobs.length > 0">
|
||||||
|
<div v-for="job in jobs" :key="job[0]">
|
||||||
|
<span>{{ job[0] }}</span>
|
||||||
|
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
|
||||||
<MkQueueChart :domain="domain" :connection="connection"/>
|
|
||||||
</div>
|
|
||||||
<div class="jobs">
|
|
||||||
<div v-if="jobs.length > 0">
|
|
||||||
<div v-for="job in jobs" :key="job[0]">
|
|
||||||
<span>{{ job[0] }}</span>
|
|
||||||
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { markRaw, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import XChart from './queue.chart.chart.vue';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
import MkQueueChart from '@/components/queue-chart.vue';
|
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { stream } from '@/stream';
|
||||||
|
|
||||||
|
const connection = markRaw(stream.useChannel('queueStats'));
|
||||||
|
|
||||||
const activeSincePrevTick = ref(0);
|
const activeSincePrevTick = ref(0);
|
||||||
const active = ref(0);
|
const active = ref(0);
|
||||||
const waiting = ref(0);
|
|
||||||
const delayed = ref(0);
|
const delayed = ref(0);
|
||||||
|
const waiting = ref(0);
|
||||||
const jobs = ref([]);
|
const jobs = ref([]);
|
||||||
|
let chartProcess = $ref<InstanceType<typeof XChart>>();
|
||||||
|
let chartActive = $ref<InstanceType<typeof XChart>>();
|
||||||
|
let chartDelayed = $ref<InstanceType<typeof XChart>>();
|
||||||
|
let chartWaiting = $ref<InstanceType<typeof XChart>>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
domain: string,
|
domain: string;
|
||||||
connection: any,
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const onStats = (stats) => {
|
||||||
|
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
||||||
|
active.value = stats[props.domain].active;
|
||||||
|
delayed.value = stats[props.domain].delayed;
|
||||||
|
waiting.value = stats[props.domain].waiting;
|
||||||
|
|
||||||
|
chartProcess.pushData(stats[props.domain].activeSincePrevTick);
|
||||||
|
chartActive.pushData(stats[props.domain].active);
|
||||||
|
chartDelayed.pushData(stats[props.domain].delayed);
|
||||||
|
chartWaiting.pushData(stats[props.domain].waiting);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStatsLog = (statsLog) => {
|
||||||
|
const dataProcess = [];
|
||||||
|
const dataActive = [];
|
||||||
|
const dataDelayed = [];
|
||||||
|
const dataWaiting = [];
|
||||||
|
|
||||||
|
for (const stats of [...statsLog].reverse()) {
|
||||||
|
dataProcess.push(stats[props.domain].activeSincePrevTick);
|
||||||
|
dataActive.push(stats[props.domain].active);
|
||||||
|
dataDelayed.push(stats[props.domain].delayed);
|
||||||
|
dataWaiting.push(stats[props.domain].waiting);
|
||||||
|
}
|
||||||
|
|
||||||
|
chartProcess.setData(dataProcess);
|
||||||
|
chartActive.setData(dataActive);
|
||||||
|
chartDelayed.setData(dataDelayed);
|
||||||
|
chartWaiting.setData(dataWaiting);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
|
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
|
||||||
jobs.value = result;
|
jobs.value = result;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onStats = (stats) => {
|
connection.on('stats', onStats);
|
||||||
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
connection.on('statsLog', onStatsLog);
|
||||||
active.value = stats[props.domain].active;
|
connection.send('requestLog', {
|
||||||
waiting.value = stats[props.domain].waiting;
|
id: Math.random().toString().substr(2, 8),
|
||||||
delayed.value = stats[props.domain].delayed;
|
length: 200,
|
||||||
};
|
|
||||||
|
|
||||||
props.connection.on('stats', onStats);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
props.connection.off('stats', onStats);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
connection.off('stats', onStats);
|
||||||
|
connection.off('statsLog', onStatsLog);
|
||||||
|
connection.dispose();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.pumxzjhg {
|
.pumxzjhg {
|
||||||
> .status {
|
> .status {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: solid 0.5px var(--divider);
|
}
|
||||||
|
|
||||||
|
> .charts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
> .chart {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .jobs {
|
> .jobs {
|
||||||
|
margin-top: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-top: solid 0.5px var(--divider);
|
|
||||||
max-height: 180px;
|
max-height: 180px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800">
|
||||||
<XQueue :connection="connection" domain="inbox">
|
<XQueue v-if="tab === 'deliver'" domain="deliver"/>
|
||||||
<template #title>In</template>
|
<XQueue v-else-if="tab === 'inbox'" domain="inbox"/>
|
||||||
</XQueue>
|
|
||||||
<XQueue :connection="connection" domain="deliver">
|
|
||||||
<template #title>Out</template>
|
|
||||||
</XQueue>
|
|
||||||
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
|
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -19,12 +14,11 @@ import XQueue from './queue.chart.vue';
|
||||||
import XHeader from './_header_.vue';
|
import XHeader from './_header_.vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
|
||||||
import * as config from '@/config';
|
import * as config from '@/config';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
const connection = markRaw(stream.useChannel('queueStats'));
|
let tab = $ref('deliver');
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
os.confirm({
|
os.confirm({
|
||||||
|
@ -38,19 +32,6 @@ function clear() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
nextTick(() => {
|
|
||||||
connection.send('requestLog', {
|
|
||||||
id: Math.random().toString().substr(2, 8),
|
|
||||||
length: 200,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
connection.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerActions = $computed(() => [{
|
const headerActions = $computed(() => [{
|
||||||
asFullButton: true,
|
asFullButton: true,
|
||||||
icon: 'fas fa-up-right-from-square',
|
icon: 'fas fa-up-right-from-square',
|
||||||
|
@ -60,7 +41,13 @@ const headerActions = $computed(() => [{
|
||||||
},
|
},
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => [{
|
||||||
|
key: 'deliver',
|
||||||
|
title: 'Deliver',
|
||||||
|
}, {
|
||||||
|
key: 'inbox',
|
||||||
|
title: 'Inbox',
|
||||||
|
}]);
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.jobQueue,
|
title: i18n.ts.jobQueue,
|
||||||
|
|
Loading…
Reference in New Issue