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