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: Add new gabber kick sounds (thanks for noizenecio) | ||||||
| - Client: Compress non-animated PNG files @saschanaz | - Client: Compress non-animated PNG files @saschanaz | ||||||
| - Client: Youtube window player @sim1222 | - Client: Youtube window player @sim1222 | ||||||
|  | - Client: enhance dashboard of control panel @syuilo | ||||||
| 
 | 
 | ||||||
| ### Bugfixes | ### Bugfixes | ||||||
| - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 | - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 | ||||||
|  |  | ||||||
|  | @ -38,6 +38,7 @@ import gradient from 'chartjs-plugin-gradient'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||||
|  | import { chartVLine } from '@/scripts/chart-vline'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
| 	src: { | 	src: { | ||||||
|  | @ -311,27 +312,7 @@ const render = () => { | ||||||
| 				gradient, | 				gradient, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		plugins: [{ | 		plugins: [chartVLine(vLineColor)], | ||||||
| 			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(); |  | ||||||
| 				} |  | ||||||
| 			}, |  | ||||||
| 		}], |  | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										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> | <template> | ||||||
| <div class="wbrkwale"> | <div> | ||||||
| 	<MkLoading v-if="fetching"/> | 	<MkLoading v-if="fetching"/> | ||||||
| 	<transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances"> | 	<div v-show="!fetching" :class="$style.root"> | ||||||
| 		<MkA v-for="(instance, i) in instances" :key="instance.id" :to="`/instance-info/${instance.host}`" class="instance"> | 		<div v-if="topSubInstancesForPie && topPubInstancesForPie" class="pies"> | ||||||
| 			<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/> | 			<div class="pie deliver _panel"> | ||||||
| 			<div class="body"> | 				<div class="title">Sub</div> | ||||||
| 				<div class="name">{{ instance.name ?? instance.host }}</div> | 				<XPie :data="topSubInstancesForPie" class="chart"/> | ||||||
| 				<div class="host">{{ instance.host }}</div> | 				<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> | 	</div> | ||||||
| 			<MkMiniChart class="chart" :src="charts[i].requests.received"/> |  | ||||||
| 		</MkA> |  | ||||||
| 	</transition-group> |  | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { onMounted, onUnmounted, ref } from 'vue'; | import { onMounted, onUnmounted, ref } from 'vue'; | ||||||
|  | import XPie from './overview.pie.vue'; | ||||||
| import MkMiniChart from '@/components/MkMiniChart.vue'; | import MkMiniChart from '@/components/MkMiniChart.vue'; | ||||||
| import * as os from '@/os'; | 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([]); | let topSubInstancesForPie: any = $ref(null); | ||||||
| const charts = ref([]); | let topPubInstancesForPie: any = $ref(null); | ||||||
| const fetching = ref(true); | 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 { handler: externalTooltipHandler } = useChartTooltip(); | ||||||
| 	const fetchedInstances = await os.api('federation/instances', { | 	 | ||||||
| 		sort: '+lastCommunicatedAt', | onMounted(async () => { | ||||||
| 		limit: 5, | 	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, { | 	fetching = false; | ||||||
| 	immediate: true, |  | ||||||
| 	afterMounted: true, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" module> | ||||||
| .wbrkwale { | .root { | ||||||
| 	> .instances { |  | ||||||
| 		.chart-move { |  | ||||||
| 			transition: transform 1s ease; |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		> .instance { | 	&:global { | ||||||
| 			display: flex; | 		> .pies { | ||||||
| 			align-items: center; | 			display: grid; | ||||||
| 			padding: 16px 20px; | 			grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); | ||||||
|  | 			grid-gap: 16px; | ||||||
|  | 			margin-bottom: 16px; | ||||||
| 
 | 
 | ||||||
| 			&:not(:last-child) { | 			> .pie { | ||||||
| 				border-bottom: solid 0.5px var(--divider); | 				position: relative; | ||||||
| 			} | 				padding: 12px; | ||||||
| 
 | 
 | ||||||
| 			> img { | 				> .title { | ||||||
| 				display: block; | 					position: absolute; | ||||||
| 				width: 34px; | 					top: 20px; | ||||||
| 				height: 34px; | 					left: 20px; | ||||||
| 				object-fit: cover; | 					font-size: 90%; | ||||||
| 				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; |  | ||||||
| 				} |  | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				> .chart { | 				> .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> | </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> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineComponent, onMounted, onUnmounted, ref } from 'vue'; | import { watch, onMounted, onUnmounted, ref } from 'vue'; | ||||||
| import { | import { | ||||||
| 	Chart, | 	Chart, | ||||||
| 	ArcElement, | 	ArcElement, | ||||||
|  | @ -25,6 +25,7 @@ import number from '@/filters/number'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||||
|  | import { chartVLine } from '@/scripts/chart-vline'; | ||||||
| 
 | 
 | ||||||
| Chart.register( | Chart.register( | ||||||
| 	ArcElement, | 	ArcElement, | ||||||
|  | @ -44,8 +45,7 @@ Chart.register( | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	domain: string; | 	type: string; | ||||||
| 	connection: any; |  | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const alpha = (hex, a) => { | const alpha = (hex, a) => { | ||||||
|  | @ -67,81 +67,59 @@ const { handler: externalTooltipHandler } = useChartTooltip(); | ||||||
| 
 | 
 | ||||||
| let chartInstance: Chart; | let chartInstance: Chart; | ||||||
| 
 | 
 | ||||||
| const onStats = (stats) => { | function setData(values) { | ||||||
|  | 	if (chartInstance == null) return; | ||||||
|  | 	for (const value of values) { | ||||||
| 		chartInstance.data.labels.push(''); | 		chartInstance.data.labels.push(''); | ||||||
| 	chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick); | 		chartInstance.data.datasets[0].data.push(value); | ||||||
| 	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 > 100) { | 		if (chartInstance.data.datasets[0].data.length > 100) { | ||||||
| 			chartInstance.data.labels.shift(); | 			chartInstance.data.labels.shift(); | ||||||
| 			chartInstance.data.datasets[0].data.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(); | 	chartInstance.update(); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| const onStatsLog = (statsLog) => { | function pushData(value) { | ||||||
| 	for (const stats of [...statsLog].reverse()) { | 	if (chartInstance == null) return; | ||||||
| 	chartInstance.data.labels.push(''); | 	chartInstance.data.labels.push(''); | ||||||
| 		chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick); | 	chartInstance.data.datasets[0].data.push(value); | ||||||
| 		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 > 100) { | 	if (chartInstance.data.datasets[0].data.length > 100) { | ||||||
| 		chartInstance.data.labels.shift(); | 		chartInstance.data.labels.shift(); | ||||||
| 		chartInstance.data.datasets[0].data.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(); | 	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(() => { | onMounted(() => { | ||||||
|  | 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||||
|  | 
 | ||||||
| 	chartInstance = new Chart(chartEl.value, { | 	chartInstance = new Chart(chartEl.value, { | ||||||
| 		type: 'line', | 		type: 'line', | ||||||
| 		data: { | 		data: { | ||||||
| 			labels: [], | 			labels: [], | ||||||
| 			datasets: [{ | 			datasets: [{ | ||||||
| 				label: 'Process', | 				label: label, | ||||||
| 				pointRadius: 0, | 				pointRadius: 0, | ||||||
| 				tension: 0.3, | 				tension: 0.3, | ||||||
| 				borderWidth: 2, | 				borderWidth: 2, | ||||||
| 				borderJoinStyle: 'round', | 				borderJoinStyle: 'round', | ||||||
| 				borderColor: '#00E396', | 				borderColor: color, | ||||||
| 				backgroundColor: alpha('#00E396', 0.1), | 				backgroundColor: alpha(color, 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: [], | 				data: [], | ||||||
| 			}], | 			}], | ||||||
| 		}, | 		}, | ||||||
|  | @ -157,9 +135,10 @@ onMounted(() => { | ||||||
| 			}, | 			}, | ||||||
| 			scales: { | 			scales: { | ||||||
| 				x: { | 				x: { | ||||||
| 					display: false, |  | ||||||
| 					grid: { | 					grid: { | ||||||
| 						display: false, | 						display: false, | ||||||
|  | 						color: gridColor, | ||||||
|  | 						borderColor: 'rgb(0, 0, 0, 0)', | ||||||
| 					}, | 					}, | ||||||
| 					ticks: { | 					ticks: { | ||||||
| 						display: false, | 						display: false, | ||||||
|  | @ -167,13 +146,10 @@ onMounted(() => { | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 				y: { | 				y: { | ||||||
| 					display: false, |  | ||||||
| 					min: 0, | 					min: 0, | ||||||
| 					grid: { | 					grid: { | ||||||
| 						display: false, | 						color: gridColor, | ||||||
| 					}, | 						borderColor: 'rgb(0, 0, 0, 0)', | ||||||
| 					ticks: { |  | ||||||
| 						display: false, |  | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
|  | @ -194,15 +170,13 @@ onMounted(() => { | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		plugins: [chartVLine(vLineColor)], | ||||||
| 	}); | 	}); | ||||||
| 
 |  | ||||||
| 	props.connection.on('stats', onStats); |  | ||||||
| 	props.connection.on('statsLog', onStatsLog); |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| onUnmounted(() => { | defineExpose({ | ||||||
| 	props.connection.off('stats', onStats); | 	setData, | ||||||
| 	props.connection.off('statsLog', onStatsLog); | 	pushData, | ||||||
| }); | }); | ||||||
| </script> | </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> | <template> | ||||||
| <MkSpacer :content-max="900"> | <MkSpacer :content-max="1000"> | ||||||
| 	<div ref="rootEl" v-size="{ max: [740] }" class="edbbcaef"> | 	<div ref="rootEl" v-size="{ max: [740] }" class="edbbcaef"> | ||||||
| 		<div class="left"> | 		<MkFolder class="item"> | ||||||
| 			<div v-if="stats" class="container stats"> | 			<template #header>Stats</template> | ||||||
| 				<div class="title">Stats</div> | 			<XStats/> | ||||||
| 				<div class="body"> | 		</MkFolder> | ||||||
| 					<div class="number _panel"> | 		<MkFolder class="item"> | ||||||
| 						<div class="label">Users</div> | 			<template #header>Active users</template> | ||||||
| 						<div class="value _monospace"> | 			<XActiveUsers/> | ||||||
| 							{{ number(stats.originalUsersCount) }} | 		</MkFolder> | ||||||
| 							<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff> | 		<MkFolder class="item"> | ||||||
| 						</div> | 			<template #header>Federation</template> | ||||||
| 					</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"> |  | ||||||
| 			<XFederation/> | 			<XFederation/> | ||||||
| 				</div> | 		</MkFolder> | ||||||
| 			</div> | 		<MkFolder class="item"> | ||||||
| 			<div v-if="stats" class="container federationStats"> | 			<template #header>Instances</template> | ||||||
| 				<div class="title">Federation</div> | 			<XInstances/> | ||||||
| 				<div class="body"> | 		</MkFolder> | ||||||
| 					<div class="number _panel"> | 		<MkFolder class="item"> | ||||||
| 						<div class="label">Sub</div> | 			<template #header>Ap requests</template> | ||||||
| 						<div class="value _monospace"> | 			<XApRequests/> | ||||||
| 							{{ number(federationSubActive) }} | 		</MkFolder> | ||||||
| 							<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff> | 		<MkFolder class="item"> | ||||||
| 						</div> | 			<template #header>New users</template> | ||||||
| 					</div> | 			<XUsers/> | ||||||
| 					<div class="number _panel"> | 		</MkFolder> | ||||||
| 						<div class="label">Pub</div> | 		<MkFolder class="item"> | ||||||
| 						<div class="value _monospace"> | 			<template #header>Deliver queue</template> | ||||||
| 							{{ number(federationPubActive) }} | 			<XQueue domain="deliver"/> | ||||||
| 							<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff> | 		</MkFolder> | ||||||
| 						</div> | 		<MkFolder class="item"> | ||||||
| 					</div> | 			<template #header>Inbox queue</template> | ||||||
| 				</div> | 			<XQueue domain="inbox"/> | ||||||
| 			</div> | 		</MkFolder> | ||||||
| 			<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> |  | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer> | ||||||
| </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 XFederation from './overview.federation.vue'; | import XFederation from './overview.federation.vue'; | ||||||
| import XQueueChart from './overview.queue-chart.vue'; | import XInstances from './overview.instances.vue'; | ||||||
| import XUser from './overview.user.vue'; | import XQueue from './overview.queue.vue'; | ||||||
| import XPie from './overview.pie.vue'; | import XApRequests from './overview.ap-requests.vue'; | ||||||
| import MkNumberDiff from '@/components/MkNumberDiff.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 MkTagCloud from '@/components/MkTagCloud.vue'; | ||||||
| import { version, url } from '@/config'; | import { version, url } from '@/config'; | ||||||
| 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 'chartjs-adapter-date-fns'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; |  | ||||||
| import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; | import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; | ||||||
| 
 | import MkFolder from '@/components/MkFolder.vue'; | ||||||
| Chart.register( |  | ||||||
| 	ArcElement, |  | ||||||
| 	LineElement, |  | ||||||
| 	BarElement, |  | ||||||
| 	PointElement, |  | ||||||
| 	BarController, |  | ||||||
| 	LineController, |  | ||||||
| 	CategoryScale, |  | ||||||
| 	LinearScale, |  | ||||||
| 	TimeScale, |  | ||||||
| 	Legend, |  | ||||||
| 	Title, |  | ||||||
| 	Tooltip, |  | ||||||
| 	SubTitle, |  | ||||||
| 	Filler, |  | ||||||
| 	//gradient, |  | ||||||
| ); |  | ||||||
| 
 | 
 | ||||||
| const rootEl = $ref<HTMLElement>(); | const rootEl = $ref<HTMLElement>(); | ||||||
| const chartEl = $ref<HTMLCanvasElement>(null); |  | ||||||
| let stats: any = $ref(null); |  | ||||||
| let serverInfo: any = $ref(null); | let serverInfo: any = $ref(null); | ||||||
| let topSubInstancesForPie: any = $ref(null); | let topSubInstancesForPie: any = $ref(null); | ||||||
| let topPubInstancesForPie: 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 federationPubActive = $ref<number | null>(null); | ||||||
| let federationPubActiveDiff = $ref<number | null>(null); | let federationPubActiveDiff = $ref<number | null>(null); | ||||||
| let federationSubActive = $ref<number | null>(null); | let federationSubActive = $ref<number | null>(null); | ||||||
|  | @ -210,170 +69,12 @@ let newUsers = $ref(null); | ||||||
| let activeInstances = $shallowRef(null); | let activeInstances = $shallowRef(null); | ||||||
| const queueStatsConnection = markRaw(stream.useChannel('queueStats')); | const queueStatsConnection = markRaw(stream.useChannel('queueStats')); | ||||||
| const now = new Date(); | const now = new Date(); | ||||||
| let chartInstance: Chart = null; |  | ||||||
| const chartLimit = 30; |  | ||||||
| const filesPagination = { | const filesPagination = { | ||||||
| 	endpoint: 'admin/drive/files' as const, | 	endpoint: 'admin/drive/files' as const, | ||||||
| 	limit: 9, | 	limit: 9, | ||||||
| 	noPaging: true, | 	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) { | function onInstanceClick(i) { | ||||||
| 	os.pageWindow(`/instance-info/${i.host}`); | 	os.pageWindow(`/instance-info/${i.host}`); | ||||||
| } | } | ||||||
|  | @ -389,20 +90,6 @@ onMounted(async () => { | ||||||
| 	magicGrid.listen(); | 	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 => { | 	os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => { | ||||||
| 		federationPubActive = chart.pubActive[0]; | 		federationPubActive = chart.pubActive[0]; | ||||||
| 		federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1]; | 		federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1]; | ||||||
|  | @ -471,165 +158,8 @@ definePageMetadata({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .edbbcaef { | .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; | 	display: grid; | ||||||
|  | 	grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); | ||||||
| 	grid-gap: 16px; | 	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> | </style> | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ import number from '@/filters/number'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | import { useChartTooltip } from '@/scripts/use-chart-tooltip'; | ||||||
|  | import { chartVLine } from '@/scripts/chart-vline'; | ||||||
| 
 | 
 | ||||||
| Chart.register( | Chart.register( | ||||||
| 	ArcElement, | 	ArcElement, | ||||||
|  | @ -105,6 +106,8 @@ const color = | ||||||
| 	'?' as never; | 	'?' as never; | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|  | 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||||
|  | 
 | ||||||
| 	chartInstance = new Chart(chartEl.value, { | 	chartInstance = new Chart(chartEl.value, { | ||||||
| 		type: 'line', | 		type: 'line', | ||||||
| 		data: { | 		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
		Add a link
		
	
		Reference in a new issue