mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 15:34:13 +00:00 
			
		
		
		
	enhance(client): enhance dashboard of control panel
This commit is contained in:
		
							parent
							
								
									c0fc0b92d3
								
							
						
					
					
						commit
						052e667f03
					
				
					 14 changed files with 1145 additions and 752 deletions
				
			
		| 
						 | 
				
			
			@ -32,6 +32,7 @@ You should also include the user name that made the change.
 | 
			
		|||
- Client: Add new gabber kick sounds (thanks for noizenecio)
 | 
			
		||||
- Client: Compress non-animated PNG files @saschanaz
 | 
			
		||||
- Client: Youtube window player @sim1222
 | 
			
		||||
- Client: enhance dashboard of control panel @syuilo
 | 
			
		||||
 | 
			
		||||
### Bugfixes
 | 
			
		||||
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ import gradient from 'chartjs-plugin-gradient';
 | 
			
		|||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
import { chartVLine } from '@/scripts/chart-vline';
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
	src: {
 | 
			
		||||
| 
						 | 
				
			
			@ -311,27 +312,7 @@ const render = () => {
 | 
			
		|||
				gradient,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		plugins: [{
 | 
			
		||||
			id: 'vLine',
 | 
			
		||||
			beforeDraw(chart, args, options) {
 | 
			
		||||
				if (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();
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
		}],
 | 
			
		||||
		plugins: [chartVLine(vLineColor)],
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										223
									
								
								packages/client/src/pages/admin/overview.active-users.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								packages/client/src/pages/admin/overview.active-users.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,223 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
	<div v-show="!fetching" :class="$style.root" class="_panel">
 | 
			
		||||
		<canvas ref="chartEl"></canvas>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
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 * as os from '@/os';
 | 
			
		||||
import 'chartjs-adapter-date-fns';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
import gradient from 'chartjs-plugin-gradient';
 | 
			
		||||
import { chartVLine } from '@/scripts/chart-vline';
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
	ArcElement,
 | 
			
		||||
	LineElement,
 | 
			
		||||
	BarElement,
 | 
			
		||||
	PointElement,
 | 
			
		||||
	BarController,
 | 
			
		||||
	LineController,
 | 
			
		||||
	CategoryScale,
 | 
			
		||||
	LinearScale,
 | 
			
		||||
	TimeScale,
 | 
			
		||||
	Legend,
 | 
			
		||||
	Title,
 | 
			
		||||
	Tooltip,
 | 
			
		||||
	SubTitle,
 | 
			
		||||
	Filler,
 | 
			
		||||
	gradient,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
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 now = new Date();
 | 
			
		||||
let chartInstance: Chart = null;
 | 
			
		||||
const chartLimit = 50;
 | 
			
		||||
let fetching = $ref(true);
 | 
			
		||||
 | 
			
		||||
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')).toHexString();
 | 
			
		||||
 | 
			
		||||
	const max = Math.max(...raw.read);
 | 
			
		||||
 | 
			
		||||
	chartInstance = new Chart(chartEl, {
 | 
			
		||||
		type: 'line',
 | 
			
		||||
		data: {
 | 
			
		||||
			//labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
 | 
			
		||||
			datasets: [{
 | 
			
		||||
				parsing: false,
 | 
			
		||||
				label: 'active',
 | 
			
		||||
				data: format(raw.read).slice().reverse(),
 | 
			
		||||
				tension: 0.3,
 | 
			
		||||
				pointRadius: 0,
 | 
			
		||||
				borderWidth: 2,
 | 
			
		||||
				borderColor: color,
 | 
			
		||||
				borderJoinStyle: 'round',
 | 
			
		||||
				//backgroundColor: alpha(color, 0.1),
 | 
			
		||||
				gradient: {
 | 
			
		||||
					backgroundColor: {
 | 
			
		||||
						axis: 'y',
 | 
			
		||||
						colors: {
 | 
			
		||||
							0: alpha(color, 0),
 | 
			
		||||
							[max]: alpha(color, 0.35),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				barPercentage: 0.9,
 | 
			
		||||
				categoryPercentage: 0.9,
 | 
			
		||||
				fill: true,
 | 
			
		||||
				clip: 8,
 | 
			
		||||
			}],
 | 
			
		||||
		},
 | 
			
		||||
		options: {
 | 
			
		||||
			aspectRatio: 2.5,
 | 
			
		||||
			layout: {
 | 
			
		||||
				padding: {
 | 
			
		||||
					left: 0,
 | 
			
		||||
					right: 8,
 | 
			
		||||
					top: 0,
 | 
			
		||||
					bottom: 0,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			scales: {
 | 
			
		||||
				x: {
 | 
			
		||||
					type: 'time',
 | 
			
		||||
					offset: false,
 | 
			
		||||
					time: {
 | 
			
		||||
						stepSize: 1,
 | 
			
		||||
						unit: 'day',
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
						color: gridColor,
 | 
			
		||||
						borderColor: 'rgb(0, 0, 0, 0)',
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: true,
 | 
			
		||||
						maxRotation: 0,
 | 
			
		||||
						autoSkipPadding: 16,
 | 
			
		||||
					},
 | 
			
		||||
					adapters: {
 | 
			
		||||
						date: {
 | 
			
		||||
							locale: enUS,
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					min: getDate(chartLimit).getTime(),
 | 
			
		||||
				},
 | 
			
		||||
				y: {
 | 
			
		||||
					position: 'left',
 | 
			
		||||
					suggestedMax: 10,
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
						color: gridColor,
 | 
			
		||||
						borderColor: 'rgb(0, 0, 0, 0)',
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: true,
 | 
			
		||||
						//mirror: true,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			interaction: {
 | 
			
		||||
				intersect: false,
 | 
			
		||||
				mode: 'index',
 | 
			
		||||
			},
 | 
			
		||||
			elements: {
 | 
			
		||||
				point: {
 | 
			
		||||
					hoverRadius: 5,
 | 
			
		||||
					hoverBorderWidth: 2,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			animation: true,
 | 
			
		||||
			plugins: {
 | 
			
		||||
				legend: {
 | 
			
		||||
					display: false,
 | 
			
		||||
				},
 | 
			
		||||
				tooltip: {
 | 
			
		||||
					enabled: false,
 | 
			
		||||
					mode: 'index',
 | 
			
		||||
					animation: {
 | 
			
		||||
						duration: 0,
 | 
			
		||||
					},
 | 
			
		||||
					external: externalTooltipHandler,
 | 
			
		||||
				},
 | 
			
		||||
				gradient,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		plugins: [chartVLine(vLineColor)],
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	fetching = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	renderChart();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										263
									
								
								packages/client/src/pages/admin/overview.ap-requests.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								packages/client/src/pages/admin/overview.ap-requests.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,263 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
	<div v-show="!fetching" :class="$style.root">
 | 
			
		||||
		<div class="chart _panel">
 | 
			
		||||
			<canvas ref="chartEl"></canvas>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { 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 gradient from 'chartjs-plugin-gradient';
 | 
			
		||||
import { enUS } from 'date-fns/locale';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import MkMiniChart from '@/components/MkMiniChart.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
import { chartVLine } from '@/scripts/chart-vline';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
	ArcElement,
 | 
			
		||||
	LineElement,
 | 
			
		||||
	BarElement,
 | 
			
		||||
	PointElement,
 | 
			
		||||
	BarController,
 | 
			
		||||
	LineController,
 | 
			
		||||
	CategoryScale,
 | 
			
		||||
	LinearScale,
 | 
			
		||||
	TimeScale,
 | 
			
		||||
	Legend,
 | 
			
		||||
	Title,
 | 
			
		||||
	Tooltip,
 | 
			
		||||
	SubTitle,
 | 
			
		||||
	Filler,
 | 
			
		||||
	gradient,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
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 chartLimit = 50;
 | 
			
		||||
const chartEl = $ref<HTMLCanvasElement>();
 | 
			
		||||
let fetching = $ref(true);
 | 
			
		||||
 | 
			
		||||
const { handler: externalTooltipHandler } = useChartTooltip();
 | 
			
		||||
	
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	const now = new Date();
 | 
			
		||||
 | 
			
		||||
	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 formatMinus = (arr) => {
 | 
			
		||||
		return arr.map((v, i) => ({
 | 
			
		||||
			x: getDate(i).getTime(),
 | 
			
		||||
			y: -v,
 | 
			
		||||
		}));
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const raw = await os.api('charts/ap-request', { 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)';
 | 
			
		||||
	const succColor = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--success')).toHexString();
 | 
			
		||||
	const failColor = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--error')).toHexString();
 | 
			
		||||
 | 
			
		||||
	const succMax = Math.max(...raw.deliverSucceeded);
 | 
			
		||||
	const failMax = Math.max(...raw.deliverFailed);
 | 
			
		||||
 | 
			
		||||
	// フォントカラー
 | 
			
		||||
	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
			
		||||
 | 
			
		||||
	new Chart(chartEl, {
 | 
			
		||||
		type: 'bar',
 | 
			
		||||
		data: {
 | 
			
		||||
			//labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
 | 
			
		||||
			datasets: [{
 | 
			
		||||
				stack: 'a',
 | 
			
		||||
				parsing: false,
 | 
			
		||||
				label: 'Succ',
 | 
			
		||||
				data: format(raw.deliverSucceeded).slice().reverse(),
 | 
			
		||||
				tension: 0.3,
 | 
			
		||||
				pointRadius: 0,
 | 
			
		||||
				borderWidth: 0,
 | 
			
		||||
				borderJoinStyle: 'round',
 | 
			
		||||
				borderRadius: 4,
 | 
			
		||||
				//backgroundColor: alpha(color, 0.1),
 | 
			
		||||
				gradient: {
 | 
			
		||||
					backgroundColor: {
 | 
			
		||||
						axis: 'y',
 | 
			
		||||
						colors: {
 | 
			
		||||
							0: alpha(succColor, 0.3),
 | 
			
		||||
							[succMax]: alpha(succColor, 1),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				barPercentage: 0.9,
 | 
			
		||||
				categoryPercentage: 0.9,
 | 
			
		||||
				fill: true,
 | 
			
		||||
				clip: 8,
 | 
			
		||||
			}, {
 | 
			
		||||
				stack: 'a',
 | 
			
		||||
				parsing: false,
 | 
			
		||||
				label: 'Fail',
 | 
			
		||||
				data: formatMinus(raw.deliverFailed).slice().reverse(),
 | 
			
		||||
				tension: 0.3,
 | 
			
		||||
				pointRadius: 0,
 | 
			
		||||
				borderWidth: 0,
 | 
			
		||||
				borderJoinStyle: 'round',
 | 
			
		||||
				borderRadius: 4,
 | 
			
		||||
				//backgroundColor: alpha(color, 0.1),
 | 
			
		||||
				gradient: {
 | 
			
		||||
					backgroundColor: {
 | 
			
		||||
						axis: 'y',
 | 
			
		||||
						colors: {
 | 
			
		||||
							0: alpha(failColor, 0.3),
 | 
			
		||||
							[-failMax]: alpha(failColor, 1),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				barPercentage: 0.9,
 | 
			
		||||
				categoryPercentage: 0.9,
 | 
			
		||||
				fill: true,
 | 
			
		||||
				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: 'day',
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
						color: gridColor,
 | 
			
		||||
						borderColor: 'rgb(0, 0, 0, 0)',
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: true,
 | 
			
		||||
						maxRotation: 0,
 | 
			
		||||
						autoSkipPadding: 16,
 | 
			
		||||
					},
 | 
			
		||||
					adapters: {
 | 
			
		||||
						date: {
 | 
			
		||||
							locale: enUS,
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					min: getDate(chartLimit).getTime(),
 | 
			
		||||
				},
 | 
			
		||||
				y: {
 | 
			
		||||
					stacked: true,
 | 
			
		||||
					position: 'left',
 | 
			
		||||
					suggestedMax: 10,
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
						color: gridColor,
 | 
			
		||||
						borderColor: 'rgb(0, 0, 0, 0)',
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: true,
 | 
			
		||||
						//mirror: true,
 | 
			
		||||
						callback: (value, index, values) => value < 0 ? -value : value,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			interaction: {
 | 
			
		||||
				intersect: false,
 | 
			
		||||
				mode: 'index',
 | 
			
		||||
			},
 | 
			
		||||
			elements: {
 | 
			
		||||
				point: {
 | 
			
		||||
					hoverRadius: 5,
 | 
			
		||||
					hoverBorderWidth: 2,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			animation: true,
 | 
			
		||||
			plugins: {
 | 
			
		||||
				legend: {
 | 
			
		||||
					display: false,
 | 
			
		||||
				},
 | 
			
		||||
				tooltip: {
 | 
			
		||||
					enabled: false,
 | 
			
		||||
					mode: 'index',
 | 
			
		||||
					animation: {
 | 
			
		||||
						duration: 0,
 | 
			
		||||
					},
 | 
			
		||||
					external: externalTooltipHandler,
 | 
			
		||||
				},
 | 
			
		||||
				gradient,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		plugins: [chartVLine(vLineColor)],
 | 
			
		||||
	});
 | 
			
		||||
	
 | 
			
		||||
	fetching = false;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
 | 
			
		||||
	&:global {
 | 
			
		||||
		> .chart {
 | 
			
		||||
			padding: 16px;
 | 
			
		||||
			margin-bottom: 16px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,100 +1,185 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="wbrkwale">
 | 
			
		||||
<div>
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
	<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
 | 
			
		||||
		<MkA v-for="(instance, i) in instances" :key="instance.id" :to="`/instance-info/${instance.host}`" class="instance">
 | 
			
		||||
			<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<div class="name">{{ instance.name ?? instance.host }}</div>
 | 
			
		||||
				<div class="host">{{ instance.host }}</div>
 | 
			
		||||
	<div v-show="!fetching" :class="$style.root">
 | 
			
		||||
		<div v-if="topSubInstancesForPie && topPubInstancesForPie" class="pies">
 | 
			
		||||
			<div class="pie deliver _panel">
 | 
			
		||||
				<div class="title">Sub</div>
 | 
			
		||||
				<XPie :data="topSubInstancesForPie" class="chart"/>
 | 
			
		||||
				<div class="subTitle">Top 10</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="pie inbox _panel">
 | 
			
		||||
				<div class="title">Pub</div>
 | 
			
		||||
				<XPie :data="topPubInstancesForPie" class="chart"/>
 | 
			
		||||
				<div class="subTitle">Top 10</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="!fetching" class="items">
 | 
			
		||||
			<div class="item _panel sub">
 | 
			
		||||
				<div class="icon"><i class="ti ti-world-download"></i></div>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<div class="value">
 | 
			
		||||
						{{ number(federationSubActive) }}
 | 
			
		||||
						<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="label">Sub</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="item _panel pub">
 | 
			
		||||
				<div class="icon"><i class="ti ti-world-upload"></i></div>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<div class="value">
 | 
			
		||||
						{{ number(federationPubActive) }}
 | 
			
		||||
						<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="label">Pub</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
			<MkMiniChart class="chart" :src="charts[i].requests.received"/>
 | 
			
		||||
		</MkA>
 | 
			
		||||
	</transition-group>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import XPie from './overview.pie.vue';
 | 
			
		||||
import MkMiniChart from '@/components/MkMiniChart.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { useInterval } from '@/scripts/use-interval';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
 | 
			
		||||
const instances = ref([]);
 | 
			
		||||
const charts = ref([]);
 | 
			
		||||
const fetching = ref(true);
 | 
			
		||||
let topSubInstancesForPie: any = $ref(null);
 | 
			
		||||
let topPubInstancesForPie: any = $ref(null);
 | 
			
		||||
let federationPubActive = $ref<number | null>(null);
 | 
			
		||||
let federationPubActiveDiff = $ref<number | null>(null);
 | 
			
		||||
let federationSubActive = $ref<number | null>(null);
 | 
			
		||||
let federationSubActiveDiff = $ref<number | null>(null);
 | 
			
		||||
let fetching = $ref(true);
 | 
			
		||||
 | 
			
		||||
const fetch = async () => {
 | 
			
		||||
	const fetchedInstances = await os.api('federation/instances', {
 | 
			
		||||
		sort: '+lastCommunicatedAt',
 | 
			
		||||
		limit: 5,
 | 
			
		||||
const { handler: externalTooltipHandler } = useChartTooltip();
 | 
			
		||||
	
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' });
 | 
			
		||||
	federationPubActive = chart.pubActive[0];
 | 
			
		||||
	federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
 | 
			
		||||
	federationSubActive = chart.subActive[0];
 | 
			
		||||
	federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
 | 
			
		||||
 | 
			
		||||
	os.apiGet('federation/stats', { limit: 10 }).then(res => {
 | 
			
		||||
		topSubInstancesForPie = res.topSubInstances.map(x => ({
 | 
			
		||||
			name: x.host,
 | 
			
		||||
			color: x.themeColor,
 | 
			
		||||
			value: x.followersCount,
 | 
			
		||||
			onClick: () => {
 | 
			
		||||
				os.pageWindow(`/instance-info/${x.host}`);
 | 
			
		||||
			},
 | 
			
		||||
		})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
 | 
			
		||||
		topPubInstancesForPie = res.topPubInstances.map(x => ({
 | 
			
		||||
			name: x.host,
 | 
			
		||||
			color: x.themeColor,
 | 
			
		||||
			value: x.followingCount,
 | 
			
		||||
			onClick: () => {
 | 
			
		||||
				os.pageWindow(`/instance-info/${x.host}`);
 | 
			
		||||
			},
 | 
			
		||||
		})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
 | 
			
		||||
	});
 | 
			
		||||
	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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
useInterval(fetch, 1000 * 60, {
 | 
			
		||||
	immediate: true,
 | 
			
		||||
	afterMounted: true,
 | 
			
		||||
	fetching = false;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.wbrkwale {
 | 
			
		||||
	> .instances {
 | 
			
		||||
		.chart-move {
 | 
			
		||||
			transition: transform 1s ease;
 | 
			
		||||
		}
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
 | 
			
		||||
		> .instance {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
			padding: 16px 20px;
 | 
			
		||||
	&:global {
 | 
			
		||||
		> .pies {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
 | 
			
		||||
			grid-gap: 16px;
 | 
			
		||||
			margin-bottom: 16px;
 | 
			
		||||
 | 
			
		||||
			&:not(:last-child) {
 | 
			
		||||
				border-bottom: solid 0.5px var(--divider);
 | 
			
		||||
			}
 | 
			
		||||
			> .pie {
 | 
			
		||||
				position: relative;
 | 
			
		||||
				padding: 12px;
 | 
			
		||||
 | 
			
		||||
			> 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;
 | 
			
		||||
 | 
			
		||||
				> .name {
 | 
			
		||||
					display: block;
 | 
			
		||||
					width: 100%;
 | 
			
		||||
					white-space: nowrap;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
					text-overflow: ellipsis;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .host {
 | 
			
		||||
					margin: 0;
 | 
			
		||||
					font-size: 75%;
 | 
			
		||||
					opacity: 0.7;
 | 
			
		||||
					white-space: nowrap;
 | 
			
		||||
					overflow: hidden;
 | 
			
		||||
					text-overflow: ellipsis;
 | 
			
		||||
				}
 | 
			
		||||
				> .title {
 | 
			
		||||
					position: absolute;
 | 
			
		||||
					top: 20px;
 | 
			
		||||
					left: 20px;
 | 
			
		||||
					font-size: 90%;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .chart {
 | 
			
		||||
				height: 30px;
 | 
			
		||||
					max-height: 150px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .subTitle {
 | 
			
		||||
					position: absolute;
 | 
			
		||||
					bottom: 20px;
 | 
			
		||||
					right: 20px;
 | 
			
		||||
					font-size: 85%;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .items {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
 | 
			
		||||
			grid-gap: 16px;
 | 
			
		||||
 | 
			
		||||
			> .item {
 | 
			
		||||
				display: flex;
 | 
			
		||||
				box-sizing: border-box;
 | 
			
		||||
				padding: 12px;
 | 
			
		||||
 | 
			
		||||
				> .icon {
 | 
			
		||||
					display: grid;
 | 
			
		||||
					place-items: center;
 | 
			
		||||
					height: 100%;
 | 
			
		||||
					aspect-ratio: 1;
 | 
			
		||||
					margin-right: 12px;
 | 
			
		||||
					background: var(--accentedBg);
 | 
			
		||||
					color: var(--accent);
 | 
			
		||||
					border-radius: 10px;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.sub {
 | 
			
		||||
					> .icon {
 | 
			
		||||
						background: #d5ba0026;
 | 
			
		||||
						color: #dfc300;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				&.pub {
 | 
			
		||||
					> .icon {
 | 
			
		||||
						background: #00cf2326;
 | 
			
		||||
						color: #00cd5b;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .body {
 | 
			
		||||
					padding: 4px 0;
 | 
			
		||||
 | 
			
		||||
					> .value {
 | 
			
		||||
						font-size: 1.3em;
 | 
			
		||||
						font-weight: bold;
 | 
			
		||||
 | 
			
		||||
						> .diff {
 | 
			
		||||
							font-size: 0.65em;
 | 
			
		||||
							font-weight: normal;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> .label {
 | 
			
		||||
						font-size: 0.8em;
 | 
			
		||||
						opacity: 0.5;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										52
									
								
								packages/client/src/pages/admin/overview.instances.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								packages/client/src/pages/admin/overview.instances.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="wbrkwale">
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
	<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
 | 
			
		||||
		<MkA v-for="(instance, i) in instances" :key="instance.id" :to="`/instance-info/${instance.host}`" class="instance">
 | 
			
		||||
			<MkInstanceCardMini :instance="instance"/>
 | 
			
		||||
		</MkA>
 | 
			
		||||
	</transition-group>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { useInterval } from '@/scripts/use-interval';
 | 
			
		||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
 | 
			
		||||
 | 
			
		||||
const instances = ref([]);
 | 
			
		||||
const fetching = ref(true);
 | 
			
		||||
 | 
			
		||||
const fetch = async () => {
 | 
			
		||||
	const fetchedInstances = await os.api('federation/instances', {
 | 
			
		||||
		sort: '+lastCommunicatedAt',
 | 
			
		||||
		limit: 6,
 | 
			
		||||
	});
 | 
			
		||||
	instances.value = fetchedInstances;
 | 
			
		||||
	fetching.value = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
useInterval(fetch, 1000 * 60, {
 | 
			
		||||
	immediate: true,
 | 
			
		||||
	afterMounted: true,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.wbrkwale {
 | 
			
		||||
	> .instances {
 | 
			
		||||
		.chart-move {
 | 
			
		||||
			transition: transform 1s ease;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
 | 
			
		||||
		grid-gap: 12px;
 | 
			
		||||
 | 
			
		||||
		> .instance:hover {
 | 
			
		||||
			text-decoration: none;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import { watch, onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import {
 | 
			
		||||
	Chart,
 | 
			
		||||
	ArcElement,
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ import number from '@/filters/number';
 | 
			
		|||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
import { chartVLine } from '@/scripts/chart-vline';
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
	ArcElement,
 | 
			
		||||
| 
						 | 
				
			
			@ -44,8 +45,7 @@ Chart.register(
 | 
			
		|||
);
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	domain: string;
 | 
			
		||||
	connection: any;
 | 
			
		||||
	type: string;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const alpha = (hex, a) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -67,81 +67,59 @@ const { handler: externalTooltipHandler } = useChartTooltip();
 | 
			
		|||
 | 
			
		||||
let chartInstance: Chart;
 | 
			
		||||
 | 
			
		||||
const onStats = (stats) => {
 | 
			
		||||
function setData(values) {
 | 
			
		||||
	if (chartInstance == null) return;
 | 
			
		||||
	for (const value of values) {
 | 
			
		||||
		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);
 | 
			
		||||
		chartInstance.data.datasets[0].data.push(value);
 | 
			
		||||
		if (chartInstance.data.datasets[0].data.length > 100) {
 | 
			
		||||
			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()) {
 | 
			
		||||
function pushData(value) {
 | 
			
		||||
	if (chartInstance == null) return;
 | 
			
		||||
	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);
 | 
			
		||||
	chartInstance.data.datasets[0].data.push(value);
 | 
			
		||||
	if (chartInstance.data.datasets[0].data.length > 100) {
 | 
			
		||||
		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 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(() => {
 | 
			
		||||
	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	chartInstance = new Chart(chartEl.value, {
 | 
			
		||||
		type: 'line',
 | 
			
		||||
		data: {
 | 
			
		||||
			labels: [],
 | 
			
		||||
			datasets: [{
 | 
			
		||||
				label: 'Process',
 | 
			
		||||
				label: label,
 | 
			
		||||
				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,
 | 
			
		||||
				borderColor: color,
 | 
			
		||||
				backgroundColor: alpha(color, 0.1),
 | 
			
		||||
				data: [],
 | 
			
		||||
			}],
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -157,9 +135,10 @@ onMounted(() => {
 | 
			
		|||
			},
 | 
			
		||||
			scales: {
 | 
			
		||||
				x: {
 | 
			
		||||
					display: false,
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
						color: gridColor,
 | 
			
		||||
						borderColor: 'rgb(0, 0, 0, 0)',
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -167,13 +146,10 @@ onMounted(() => {
 | 
			
		|||
					},
 | 
			
		||||
				},
 | 
			
		||||
				y: {
 | 
			
		||||
					display: false,
 | 
			
		||||
					min: 0,
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: false,
 | 
			
		||||
						color: gridColor,
 | 
			
		||||
						borderColor: 'rgb(0, 0, 0, 0)',
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
| 
						 | 
				
			
			@ -194,15 +170,13 @@ onMounted(() => {
 | 
			
		|||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		plugins: [chartVLine(vLineColor)],
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	props.connection.on('stats', onStats);
 | 
			
		||||
	props.connection.on('statsLog', onStatsLog);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	props.connection.off('stats', onStats);
 | 
			
		||||
	props.connection.off('statsLog', onStatsLog);
 | 
			
		||||
defineExpose({
 | 
			
		||||
	setData,
 | 
			
		||||
	pushData,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										127
									
								
								packages/client/src/pages/admin/overview.queue.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								packages/client/src/pages/admin/overview.queue.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,127 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
	<div class="_table status">
 | 
			
		||||
		<div class="_row">
 | 
			
		||||
			<div class="_cell" style="text-align: center;"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
 | 
			
		||||
			<div class="_cell" style="text-align: center;"><div class="_label">Active</div>{{ number(active) }}</div>
 | 
			
		||||
			<div class="_cell" style="text-align: center;"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
 | 
			
		||||
			<div class="_cell" style="text-align: center;"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
 | 
			
		||||
		</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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { markRaw, onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import XChart from './overview.queue.chart.vue';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const connection = markRaw(stream.useChannel('queueStats'));
 | 
			
		||||
 | 
			
		||||
const activeSincePrevTick = ref(0);
 | 
			
		||||
const active = ref(0);
 | 
			
		||||
const delayed = ref(0);
 | 
			
		||||
const waiting = ref(0);
 | 
			
		||||
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<{
 | 
			
		||||
	domain: string;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
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(() => {
 | 
			
		||||
	connection.on('stats', onStats);
 | 
			
		||||
	connection.on('statsLog', onStatsLog);
 | 
			
		||||
	connection.send('requestLog', {
 | 
			
		||||
		id: Math.random().toString().substr(2, 8),
 | 
			
		||||
		length: 100,
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	connection.off('stats', onStats);
 | 
			
		||||
	connection.off('statsLog', onStatsLog);
 | 
			
		||||
	connection.dispose();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	&:global {
 | 
			
		||||
		> .status {
 | 
			
		||||
			padding: 0 0 16px 0;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .charts {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: 1fr 1fr;
 | 
			
		||||
			gap: 16px;
 | 
			
		||||
 | 
			
		||||
			> .chart {
 | 
			
		||||
				min-width: 0;
 | 
			
		||||
				padding: 16px;
 | 
			
		||||
				background: var(--panel);
 | 
			
		||||
				border-radius: var(--radius);
 | 
			
		||||
 | 
			
		||||
				> .title {
 | 
			
		||||
					font-size: 0.85em;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>	
 | 
			
		||||
							
								
								
									
										153
									
								
								packages/client/src/pages/admin/overview.stats.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								packages/client/src/pages/admin/overview.stats.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,153 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
	<div v-else :class="$style.root">
 | 
			
		||||
		<div class="item _panel users">
 | 
			
		||||
			<div class="icon"><i class="ti ti-users"></i></div>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<div class="value">
 | 
			
		||||
					{{ number(stats.originalUsersCount) }}
 | 
			
		||||
					<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="label">Users</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="item _panel notes">
 | 
			
		||||
			<div class="icon"><i class="ti ti-pencil"></i></div>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<div class="value">
 | 
			
		||||
					{{ number(stats.originalNotesCount) }}
 | 
			
		||||
					<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="label">Notes</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="item _panel instances">
 | 
			
		||||
			<div class="icon"><i class="ti ti-planet"></i></div>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<div class="value">
 | 
			
		||||
					{{ number(stats.instances) }}
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="label">Instances</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="item _panel online">
 | 
			
		||||
			<div class="icon"><i class="ti ti-access-point"></i></div>
 | 
			
		||||
			<div class="body">
 | 
			
		||||
				<div class="value">
 | 
			
		||||
					{{ number(onlineUsersCount) }}
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="label">Online</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import MkMiniChart from '@/components/MkMiniChart.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
let stats: any = $ref(null);
 | 
			
		||||
let usersComparedToThePrevDay = $ref<number>();
 | 
			
		||||
let notesComparedToThePrevDay = $ref<number>();
 | 
			
		||||
let onlineUsersCount = $ref(0);
 | 
			
		||||
let fetching = $ref(true);
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	const [_stats, _onlineUsersCount] = await Promise.all([
 | 
			
		||||
		os.api('stats', {}),
 | 
			
		||||
		os.api('get-online-users-count').then(res => res.count),
 | 
			
		||||
	]);
 | 
			
		||||
	stats = _stats;
 | 
			
		||||
	onlineUsersCount = _onlineUsersCount;
 | 
			
		||||
 | 
			
		||||
	os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
 | 
			
		||||
		usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
 | 
			
		||||
		notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	fetching = false;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
 | 
			
		||||
	grid-gap: 16px;
 | 
			
		||||
 | 
			
		||||
	&:global {
 | 
			
		||||
		> .item {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
			padding: 12px;
 | 
			
		||||
 | 
			
		||||
			> .icon {
 | 
			
		||||
				display: grid;
 | 
			
		||||
				place-items: center;
 | 
			
		||||
				height: 100%;
 | 
			
		||||
				aspect-ratio: 1;
 | 
			
		||||
				margin-right: 12px;
 | 
			
		||||
				background: var(--accentedBg);
 | 
			
		||||
				color: var(--accent);
 | 
			
		||||
				border-radius: 10px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.users {
 | 
			
		||||
				> .icon {
 | 
			
		||||
					background: #0088d726;
 | 
			
		||||
					color: #3d96c1;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.notes {
 | 
			
		||||
				> .icon {
 | 
			
		||||
					background: #86b30026;
 | 
			
		||||
					color: #86b300;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.instances {
 | 
			
		||||
				> .icon {
 | 
			
		||||
					background: #e96b0026;
 | 
			
		||||
					color: #d76d00;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.online {
 | 
			
		||||
				> .icon {
 | 
			
		||||
					background: #8a00d126;
 | 
			
		||||
					color: #c01ac3;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .body {
 | 
			
		||||
				padding: 4px 0;
 | 
			
		||||
 | 
			
		||||
				> .value {
 | 
			
		||||
					font-size: 1.3em;
 | 
			
		||||
					font-weight: bold;
 | 
			
		||||
 | 
			
		||||
					> .diff {
 | 
			
		||||
						font-size: 0.65em;
 | 
			
		||||
						font-weight: normal;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				> .label {
 | 
			
		||||
					font-size: 0.8em;
 | 
			
		||||
					opacity: 0.5;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,76 +0,0 @@
 | 
			
		|||
<template>
 | 
			
		||||
<MkA :class="[$style.root]" :to="`/user-info/${user.id}`">
 | 
			
		||||
	<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
 | 
			
		||||
	<div class="body">
 | 
			
		||||
		<span class="name"><MkUserName class="name" :user="user"/></span>
 | 
			
		||||
		<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkMiniChart v-if="chart" class="chart" :src="chart.inc"/>
 | 
			
		||||
</MkA>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import MkMiniChart from '@/components/MkMiniChart.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { acct } from '@/filters/user';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	user: misskey.entities.User;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let chart = $ref(null);
 | 
			
		||||
 | 
			
		||||
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => {
 | 
			
		||||
	chart = res;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	$bodyTitleHieght: 18px;
 | 
			
		||||
	$bodyInfoHieght: 16px;
 | 
			
		||||
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
 | 
			
		||||
	> :global(.avatar) {
 | 
			
		||||
		display: block;
 | 
			
		||||
		width: ($bodyTitleHieght + $bodyInfoHieght);
 | 
			
		||||
		height: ($bodyTitleHieght + $bodyInfoHieght);
 | 
			
		||||
		margin-right: 12px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> :global(.body) {
 | 
			
		||||
		flex: 1;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
		font-size: 0.9em;
 | 
			
		||||
		color: var(--fg);
 | 
			
		||||
		padding-right: 8px;
 | 
			
		||||
 | 
			
		||||
		> :global(.name) {
 | 
			
		||||
			display: block;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			white-space: nowrap;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
			text-overflow: ellipsis;
 | 
			
		||||
			line-height: $bodyTitleHieght;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> :global(.sub) {
 | 
			
		||||
			display: block;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			font-size: 95%;
 | 
			
		||||
			opacity: 0.7;
 | 
			
		||||
			line-height: $bodyInfoHieght;
 | 
			
		||||
			white-space: nowrap;
 | 
			
		||||
			overflow: hidden;
 | 
			
		||||
			text-overflow: ellipsis;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> :global(.chart) {
 | 
			
		||||
		height: 30px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										55
									
								
								packages/client/src/pages/admin/overview.users.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								packages/client/src/pages/admin/overview.users.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
	<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="users">
 | 
			
		||||
		<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/user-info/${user.id}`" class="user">
 | 
			
		||||
			<MkUserCardMini :user="user"/>
 | 
			
		||||
		</MkA>
 | 
			
		||||
	</transition-group>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { useInterval } from '@/scripts/use-interval';
 | 
			
		||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
 | 
			
		||||
 | 
			
		||||
let newUsers = $ref(null);
 | 
			
		||||
let fetching = $ref(true);
 | 
			
		||||
 | 
			
		||||
const fetch = async () => {
 | 
			
		||||
	const _newUsers = await os.api('admin/show-users', {
 | 
			
		||||
		limit: 5,
 | 
			
		||||
		sort: '+createdAt',
 | 
			
		||||
		origin: 'local',
 | 
			
		||||
	});
 | 
			
		||||
	newUsers = _newUsers;
 | 
			
		||||
	fetching = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
useInterval(fetch, 1000 * 60, {
 | 
			
		||||
	immediate: true,
 | 
			
		||||
	afterMounted: true,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	&:global {
 | 
			
		||||
		> .users {
 | 
			
		||||
			.chart-move {
 | 
			
		||||
				transition: transform 1s ease;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
 | 
			
		||||
			grid-gap: 12px;
 | 
			
		||||
 | 
			
		||||
			> .user:hover {
 | 
			
		||||
				text-decoration: none;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,207 +1,66 @@
 | 
			
		|||
<template>
 | 
			
		||||
<MkSpacer :content-max="900">
 | 
			
		||||
<MkSpacer :content-max="1000">
 | 
			
		||||
	<div ref="rootEl" v-size="{ max: [740] }" class="edbbcaef">
 | 
			
		||||
		<div class="left">
 | 
			
		||||
			<div v-if="stats" class="container stats">
 | 
			
		||||
				<div class="title">Stats</div>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<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">
 | 
			
		||||
					<div class="chart deliver">
 | 
			
		||||
						<div class="title">Deliver</div>
 | 
			
		||||
						<XQueueChart :connection="queueStatsConnection" domain="deliver"/>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="chart inbox">
 | 
			
		||||
						<div class="title">Inbox</div>
 | 
			
		||||
						<XQueueChart :connection="queueStatsConnection" domain="inbox"/>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="container users">
 | 
			
		||||
				<div class="title">New users</div>
 | 
			
		||||
				<div v-if="newUsers" class="body">
 | 
			
		||||
					<XUser v-for="user in newUsers" :key="user.id" class="user" :user="user"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="container files">
 | 
			
		||||
				<div class="title">Recent files</div>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="container env">
 | 
			
		||||
				<div class="title">Environment</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 class="right">
 | 
			
		||||
			<div class="container charts">
 | 
			
		||||
				<div class="title">Active users</div>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<canvas ref="chartEl"></canvas>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="container federation">
 | 
			
		||||
				<div class="title">Active instances</div>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>Stats</template>
 | 
			
		||||
			<XStats/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>Active users</template>
 | 
			
		||||
			<XActiveUsers/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>Federation</template>
 | 
			
		||||
			<XFederation/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="stats" class="container federationStats">
 | 
			
		||||
				<div class="title">Federation</div>
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<div class="number _panel">
 | 
			
		||||
						<div class="label">Sub</div>
 | 
			
		||||
						<div class="value _monospace">
 | 
			
		||||
							{{ number(federationSubActive) }}
 | 
			
		||||
							<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="number _panel">
 | 
			
		||||
						<div class="label">Pub</div>
 | 
			
		||||
						<div class="value _monospace">
 | 
			
		||||
							{{ number(federationPubActive) }}
 | 
			
		||||
							<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="container tagCloud">
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<MkTagCloud v-if="activeInstances">
 | 
			
		||||
						<li v-for="instance in activeInstances">
 | 
			
		||||
							<a @click.prevent="onInstanceClick(instance)">
 | 
			
		||||
								<img style="width: 32px;" :src="instance.iconUrl">
 | 
			
		||||
							</a>
 | 
			
		||||
						</li>
 | 
			
		||||
					</MkTagCloud>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="topSubInstancesForPie && topPubInstancesForPie" class="container federationPies">
 | 
			
		||||
				<div class="body">
 | 
			
		||||
					<div class="chart deliver">
 | 
			
		||||
						<div class="title">Sub</div>
 | 
			
		||||
						<XPie :data="topSubInstancesForPie"/>
 | 
			
		||||
						<div class="subTitle">Top 10</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="chart inbox">
 | 
			
		||||
						<div class="title">Pub</div>
 | 
			
		||||
						<XPie :data="topPubInstancesForPie"/>
 | 
			
		||||
						<div class="subTitle">Top 10</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>Instances</template>
 | 
			
		||||
			<XInstances/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>Ap requests</template>
 | 
			
		||||
			<XApRequests/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>New users</template>
 | 
			
		||||
			<XUsers/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>Deliver queue</template>
 | 
			
		||||
			<XQueue domain="deliver"/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
		<MkFolder class="item">
 | 
			
		||||
			<template #header>Inbox queue</template>
 | 
			
		||||
			<XQueue domain="inbox"/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
	</div>
 | 
			
		||||
</MkSpacer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
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 XFederation from './overview.federation.vue';
 | 
			
		||||
import XQueueChart from './overview.queue-chart.vue';
 | 
			
		||||
import XUser from './overview.user.vue';
 | 
			
		||||
import XPie from './overview.pie.vue';
 | 
			
		||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
 | 
			
		||||
import XInstances from './overview.instances.vue';
 | 
			
		||||
import XQueue from './overview.queue.vue';
 | 
			
		||||
import XApRequests from './overview.ap-requests.vue';
 | 
			
		||||
import XUsers from './overview.users.vue';
 | 
			
		||||
import XActiveUsers from './overview.active-users.vue';
 | 
			
		||||
import XStats from './overview.stats.vue';
 | 
			
		||||
import MkTagCloud from '@/components/MkTagCloud.vue';
 | 
			
		||||
import { version, url } from '@/config';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { stream } from '@/stream';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
			
		||||
import 'chartjs-adapter-date-fns';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
	ArcElement,
 | 
			
		||||
	LineElement,
 | 
			
		||||
	BarElement,
 | 
			
		||||
	PointElement,
 | 
			
		||||
	BarController,
 | 
			
		||||
	LineController,
 | 
			
		||||
	CategoryScale,
 | 
			
		||||
	LinearScale,
 | 
			
		||||
	TimeScale,
 | 
			
		||||
	Legend,
 | 
			
		||||
	Title,
 | 
			
		||||
	Tooltip,
 | 
			
		||||
	SubTitle,
 | 
			
		||||
	Filler,
 | 
			
		||||
	//gradient,
 | 
			
		||||
);
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
 | 
			
		||||
const rootEl = $ref<HTMLElement>();
 | 
			
		||||
const chartEl = $ref<HTMLCanvasElement>(null);
 | 
			
		||||
let stats: any = $ref(null);
 | 
			
		||||
let serverInfo: any = $ref(null);
 | 
			
		||||
let topSubInstancesForPie: any = $ref(null);
 | 
			
		||||
let topPubInstancesForPie: any = $ref(null);
 | 
			
		||||
let usersComparedToThePrevDay: any = $ref(null);
 | 
			
		||||
let notesComparedToThePrevDay: any = $ref(null);
 | 
			
		||||
let federationPubActive = $ref<number | null>(null);
 | 
			
		||||
let federationPubActiveDiff = $ref<number | null>(null);
 | 
			
		||||
let federationSubActive = $ref<number | null>(null);
 | 
			
		||||
| 
						 | 
				
			
			@ -210,170 +69,12 @@ let newUsers = $ref(null);
 | 
			
		|||
let activeInstances = $shallowRef(null);
 | 
			
		||||
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
 | 
			
		||||
const now = new Date();
 | 
			
		||||
let chartInstance: Chart = null;
 | 
			
		||||
const chartLimit = 30;
 | 
			
		||||
const filesPagination = {
 | 
			
		||||
	endpoint: 'admin/drive/files' as const,
 | 
			
		||||
	limit: 9,
 | 
			
		||||
	noPaging: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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: 0,
 | 
			
		||||
					top: 0,
 | 
			
		||||
					bottom: 0,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			scales: {
 | 
			
		||||
				x: {
 | 
			
		||||
					type: 'time',
 | 
			
		||||
					display: false,
 | 
			
		||||
					stacked: true,
 | 
			
		||||
					offset: false,
 | 
			
		||||
					time: {
 | 
			
		||||
						stepSize: 1,
 | 
			
		||||
						unit: 'month',
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
						display: false,
 | 
			
		||||
					},
 | 
			
		||||
					ticks: {
 | 
			
		||||
						display: false,
 | 
			
		||||
					},
 | 
			
		||||
					adapters: {
 | 
			
		||||
						date: {
 | 
			
		||||
							locale: enUS,
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					min: getDate(chartLimit).getTime(),
 | 
			
		||||
				},
 | 
			
		||||
				y: {
 | 
			
		||||
					display: false,
 | 
			
		||||
					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?.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();
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
		}],
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onInstanceClick(i) {
 | 
			
		||||
	os.pageWindow(`/instance-info/${i.host}`);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -389,20 +90,6 @@ onMounted(async () => {
 | 
			
		|||
	magicGrid.listen();
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	renderChart();
 | 
			
		||||
 | 
			
		||||
	os.api('stats', {}).then(statsResponse => {
 | 
			
		||||
		stats = statsResponse;
 | 
			
		||||
 | 
			
		||||
		os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
 | 
			
		||||
			usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
 | 
			
		||||
			notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
 | 
			
		||||
		federationPubActive = chart.pubActive[0];
 | 
			
		||||
		federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
 | 
			
		||||
| 
						 | 
				
			
			@ -471,165 +158,8 @@ definePageMetadata({
 | 
			
		|||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.edbbcaef {
 | 
			
		||||
	display: flex;
 | 
			
		||||
 | 
			
		||||
	> .left, > .right {
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		width: 50%;
 | 
			
		||||
 | 
			
		||||
		> .container {
 | 
			
		||||
			margin: 32px 0;
 | 
			
		||||
 | 
			
		||||
			> .title {
 | 
			
		||||
				font-weight: bold;
 | 
			
		||||
				margin-bottom: 16px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.stats, &.federationStats {
 | 
			
		||||
				> .body {
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
 | 
			
		||||
	grid-gap: 16px;
 | 
			
		||||
					grid-template-columns: repeat(auto-fill, minmax(180px, 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.7em;
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.env {
 | 
			
		||||
				> .body {
 | 
			
		||||
					display: grid;
 | 
			
		||||
					grid-gap: 16px;
 | 
			
		||||
					grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
 | 
			
		||||
 | 
			
		||||
					> .number {
 | 
			
		||||
						padding: 14px 20px;
 | 
			
		||||
 | 
			
		||||
						> .label {
 | 
			
		||||
							opacity: 0.7;
 | 
			
		||||
							font-size: 0.8em;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> .value {
 | 
			
		||||
							font-size: 1.1em;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.charts {
 | 
			
		||||
				> .body {
 | 
			
		||||
					padding: 32px;
 | 
			
		||||
					background: var(--panel);
 | 
			
		||||
					border-radius: var(--radius);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.users {
 | 
			
		||||
				> .body {
 | 
			
		||||
					background: var(--panel);
 | 
			
		||||
					border-radius: var(--radius);
 | 
			
		||||
 | 
			
		||||
					> .user {
 | 
			
		||||
						padding: 16px 20px;
 | 
			
		||||
 | 
			
		||||
						&:not(:last-child) {
 | 
			
		||||
							border-bottom: solid 0.5px var(--divider);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.federation {
 | 
			
		||||
				> .body {
 | 
			
		||||
					background: var(--panel);
 | 
			
		||||
					border-radius: var(--radius);
 | 
			
		||||
					overflow: clip;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.queue {
 | 
			
		||||
				> .body {
 | 
			
		||||
					display: grid;
 | 
			
		||||
					grid-gap: 16px;
 | 
			
		||||
					grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
 | 
			
		||||
 | 
			
		||||
					> .chart {
 | 
			
		||||
						position: relative;
 | 
			
		||||
						padding: 20px;
 | 
			
		||||
						background: var(--panel);
 | 
			
		||||
						border-radius: var(--radius);
 | 
			
		||||
 | 
			
		||||
						> .title {
 | 
			
		||||
							position: absolute;
 | 
			
		||||
							top: 20px;
 | 
			
		||||
							left: 20px;
 | 
			
		||||
							font-size: 90%;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.federationPies {
 | 
			
		||||
				> .body {
 | 
			
		||||
					display: grid;
 | 
			
		||||
					grid-gap: 16px;
 | 
			
		||||
					grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
 | 
			
		||||
 | 
			
		||||
					> .chart {
 | 
			
		||||
						position: relative;
 | 
			
		||||
						padding: 20px;
 | 
			
		||||
						background: var(--panel);
 | 
			
		||||
						border-radius: var(--radius);
 | 
			
		||||
 | 
			
		||||
						> .title {
 | 
			
		||||
							position: absolute;
 | 
			
		||||
							top: 20px;
 | 
			
		||||
							left: 20px;
 | 
			
		||||
							font-size: 90%;
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						> .subTitle {
 | 
			
		||||
							position: absolute;
 | 
			
		||||
							bottom: 20px;
 | 
			
		||||
							right: 20px;
 | 
			
		||||
							font-size: 85%;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.tagCloud {
 | 
			
		||||
				> .body {
 | 
			
		||||
					background: var(--panel);
 | 
			
		||||
					border-radius: var(--radius);
 | 
			
		||||
					overflow: clip;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .left {
 | 
			
		||||
		padding-right: 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .right {
 | 
			
		||||
		padding-left: 16px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ import number from '@/filters/number';
 | 
			
		|||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
import { chartVLine } from '@/scripts/chart-vline';
 | 
			
		||||
 | 
			
		||||
Chart.register(
 | 
			
		||||
	ArcElement,
 | 
			
		||||
| 
						 | 
				
			
			@ -105,6 +106,8 @@ const color =
 | 
			
		|||
	'?' as never;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	chartInstance = new Chart(chartEl.value, {
 | 
			
		||||
		type: 'line',
 | 
			
		||||
		data: {
 | 
			
		||||
| 
						 | 
				
			
			@ -167,6 +170,7 @@ onMounted(() => {
 | 
			
		|||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		plugins: [chartVLine(vLineColor)],
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										21
									
								
								packages/client/src/scripts/chart-vline.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								packages/client/src/scripts/chart-vline.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
export const chartVLine = (vLineColor: string) => ({
 | 
			
		||||
	id: 'vLine',
 | 
			
		||||
	beforeDraw(chart, args, options) {
 | 
			
		||||
		if (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();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue