diff options
Diffstat (limited to 'src/client/pages/admin/queue.chart.vue')
| -rw-r--r-- | src/client/pages/admin/queue.chart.vue | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/src/client/pages/admin/queue.chart.vue b/src/client/pages/admin/queue.chart.vue new file mode 100644 index 0000000000..4f8fd762bb --- /dev/null +++ b/src/client/pages/admin/queue.chart.vue @@ -0,0 +1,218 @@ +<template> +<div class="_debobigegoItem"> + <div class="_debobigegoLabel"><slot name="title"></slot></div> + <div class="_debobigegoPanel pumxzjhg"> + <div class="_table status"> + <div class="_row"> + <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> + <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> + <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> + <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> + </div> + </div> + <div class=""> + <canvas ref="chart"></canvas> + </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> +</template> + +<script lang="ts"> +import { defineComponent, markRaw } from 'vue'; +import Chart from 'chart.js'; +import number from '@client/filters/number'; + +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})`; +}; +import * as os from '@client/os'; + +export default defineComponent({ + props: { + domain: { + required: true + }, + connection: { + required: true + }, + }, + + data() { + return { + chart: null, + jobs: [], + activeSincePrevTick: 0, + active: 0, + waiting: 0, + delayed: 0, + } + }, + + mounted() { + this.fetchJobs(); + + // TODO: var(--panel)の色が暗いか明るいかで判定する + const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; + + Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + + this.chart = markRaw(new Chart(this.$refs.chart, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Process', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#00E396', + backgroundColor: alpha('#00E396', 0.1), + data: [] + }, { + label: 'Active', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#00BCD4', + backgroundColor: alpha('#00BCD4', 0.1), + data: [] + }, { + label: 'Waiting', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#FFB300', + backgroundColor: alpha('#FFB300', 0.1), + data: [] + }, { + label: 'Delayed', + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: '#E53935', + borderDash: [5, 5], + fill: false, + data: [] + }] + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 16, + right: 16, + top: 16, + bottom: 12 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false, + color: gridColor, + zeroLineColor: gridColor, + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + gridLines: { + display: true, + color: gridColor, + zeroLineColor: gridColor, + }, + ticks: { + display: false, + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + })); + + this.connection.on('stats', this.onStats); + this.connection.on('statsLog', this.onStatsLog); + }, + + beforeUnmount() { + this.connection.off('stats', this.onStats); + this.connection.off('statsLog', this.onStatsLog); + }, + + methods: { + onStats(stats) { + this.activeSincePrevTick = stats[this.domain].activeSincePrevTick; + this.active = stats[this.domain].active; + this.waiting = stats[this.domain].waiting; + this.delayed = stats[this.domain].delayed; + this.chart.data.labels.push(''); + this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick); + this.chart.data.datasets[1].data.push(stats[this.domain].active); + this.chart.data.datasets[2].data.push(stats[this.domain].waiting); + this.chart.data.datasets[3].data.push(stats[this.domain].delayed); + if (this.chart.data.datasets[0].data.length > 200) { + this.chart.data.labels.shift(); + this.chart.data.datasets[0].data.shift(); + this.chart.data.datasets[1].data.shift(); + this.chart.data.datasets[2].data.shift(); + this.chart.data.datasets[3].data.shift(); + } + this.chart.update(); + }, + + onStatsLog(statsLog) { + for (const stats of [...statsLog].reverse()) { + this.onStats(stats); + } + }, + + fetchJobs() { + os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => { + this.jobs = jobs; + }); + }, + + number + } +}); +</script> + +<style lang="scss" scoped> +.pumxzjhg { + > .status { + padding: 16px; + border-bottom: solid 0.5px var(--divider); + } + + > .jobs { + padding: 16px; + border-top: solid 0.5px var(--divider); + max-height: 180px; + overflow: auto; + } +} +</style> |