summaryrefslogtreecommitdiff
path: root/src/client/pages/admin/queue.chart.vue
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/pages/admin/queue.chart.vue')
-rw-r--r--src/client/pages/admin/queue.chart.vue218
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>