<template> <div> <ui-card> <template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template> <section class="wptihjuy"> <header><fa :icon="faPaperPlane"/> Deliver</header> <ui-info warn v-if="latestStats && latestStats.deliver.waiting > 0">The queue is jammed.</ui-info> <ui-horizon-group inputs v-if="latestStats" class="fit-bottom"> <ui-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly> <span>Process</span> <template #prefix><fa :icon="fasPlayCircle"/></template> <template #suffix>jobs/tick</template> </ui-input> <ui-input :value="latestStats.deliver.active | number" type="text" readonly> <span>Active</span> <template #prefix><fa :icon="farPlayCircle"/></template> <template #suffix>jobs</template> </ui-input> <ui-input :value="latestStats.deliver.waiting | number" type="text" readonly> <span>Waiting</span> <template #prefix><fa :icon="faStopCircle"/></template> <template #suffix>jobs</template> </ui-input> <ui-input :value="latestStats.deliver.delayed | number" type="text" readonly> <span>Delayed</span> <template #prefix><fa :icon="faStopwatch"/></template> <template #suffix>jobs</template> </ui-input> </ui-horizon-group> <div ref="deliverChart" class="chart"></div> </section> <section class="wptihjuy"> <header><fa :icon="faInbox"/> Inbox</header> <ui-info warn v-if="latestStats && latestStats.inbox.waiting > 0">The queue is jammed.</ui-info> <ui-horizon-group inputs v-if="latestStats" class="fit-bottom"> <ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly> <span>Process</span> <template #prefix><fa :icon="fasPlayCircle"/></template> <template #suffix>jobs/tick</template> </ui-input> <ui-input :value="latestStats.inbox.active | number" type="text" readonly> <span>Active</span> <template #prefix><fa :icon="farPlayCircle"/></template> <template #suffix>jobs</template> </ui-input> <ui-input :value="latestStats.inbox.waiting | number" type="text" readonly> <span>Waiting</span> <template #prefix><fa :icon="faStopCircle"/></template> <template #suffix>jobs</template> </ui-input> <ui-input :value="latestStats.inbox.delayed | number" type="text" readonly> <span>Delayed</span> <template #prefix><fa :icon="faStopwatch"/></template> <template #suffix>jobs</template> </ui-input> </ui-horizon-group> <div ref="inboxChart" class="chart"></div> </section> <section> <ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button> </section> </ui-card> <ui-card> <template #title><fa :icon="faTasks"/> {{ $t('jobs') }}</template> <section class="fit-top"> <ui-horizon-group inputs> <ui-select v-model="domain"> <template #label>{{ $t('queue') }}</template> <option value="deliver">{{ $t('domains.deliver') }}</option> <option value="inbox">{{ $t('domains.inbox') }}</option> </ui-select> <ui-select v-model="state"> <template #label>{{ $t('state') }}</template> <option value="delayed">{{ $t('states.delayed') }}</option> </ui-select> </ui-horizon-group> <sequential-entrance animation="entranceFromTop" delay="25"> <div class="xvvuvgsv" v-for="job in jobs"> <b>{{ job.id }}</b> <template v-if="domain === 'deliver'"> <span>{{ job.data.to }}</span> </template> <template v-if="domain === 'inbox'"> <span>{{ job.activity.id }}</span> </template> </div> </sequential-entrance> <ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info> </section> </ui-card> </div> </template> <script lang="ts"> import Vue from 'vue'; import i18n from '../../i18n'; import ApexCharts from 'apexcharts'; import * as tinycolor from 'tinycolor2'; import { faTasks, faInbox, faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons'; import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle, faChartBar } from '@fortawesome/free-regular-svg-icons'; const limit = 200; export default Vue.extend({ i18n: i18n('admin/views/queue.vue'), data() { return { stats: [], deliverChart: null, inboxChart: null, jobs: [], jobsLimit: 50, domain: 'deliver', state: 'delayed', faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle, faChartBar }; }, computed: { latestStats(): any { return this.stats[this.stats.length - 1]; } }, watch: { stats(stats) { this.inboxChart.updateSeries([{ name: 'Process', type: 'area', data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick })) }, { name: 'Active', type: 'area', data: stats.map((x, i) => ({ x: i, y: x.inbox.active })) }, { name: 'Waiting', type: 'line', data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting })) }, { name: 'Delayed', type: 'line', data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed })) }]); this.deliverChart.updateSeries([{ name: 'Process', type: 'area', data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick })) }, { name: 'Active', type: 'area', data: stats.map((x, i) => ({ x: i, y: x.deliver.active })) }, { name: 'Waiting', type: 'line', data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting })) }, { name: 'Delayed', type: 'line', data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed })) }]); }, domain() { this.jobs = []; this.fetchJobs(); }, state() { this.jobs = []; this.fetchJobs(); }, }, mounted() { this.fetchJobs(); const chartOpts = id => ({ chart: { id, group: 'queue', type: 'area', height: 200, animations: { dynamicAnimation: { enabled: false } }, toolbar: { show: false }, zoom: { enabled: false } }, dataLabels: { enabled: false }, grid: { clipMarkers: false, borderColor: 'rgba(0, 0, 0, 0.1)', xaxis: { lines: { show: true, } }, }, stroke: { curve: 'straight', width: 2 }, tooltip: { enabled: false }, legend: { labels: { colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() }, }, series: [] as any, colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'], xaxis: { type: 'numeric', labels: { show: false }, tooltip: { enabled: false } }, yaxis: { show: false, min: 0, } }); this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts('a')); this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts('b')); this.inboxChart.render(); this.deliverChart.render(); const connection = this.$root.stream.useSharedConnection('queueStats'); connection.on('stats', this.onStats); connection.on('statsLog', this.onStatsLog); connection.send('requestLog', { id: Math.random().toString().substr(2, 8), length: limit }); this.$once('hook:beforeDestroy', () => { connection.dispose(); this.inboxChart.destroy(); this.deliverChart.destroy(); }); }, methods: { async removeAllJobs() { const process = async () => { await this.$root.api('admin/queue/clear'); this.$root.dialog({ type: 'success', splash: true }); }; await process().catch(e => { this.$root.dialog({ type: 'error', text: e.toString() }); }); }, onStats(stats) { this.stats.push(stats); if (this.stats.length > limit) this.stats.shift(); }, onStatsLog(statsLog) { for (const stats of statsLog.reverse()) { this.onStats(stats); } }, fetchJobs() { this.$root.api('admin/queue/jobs', { domain: this.domain, state: this.state, limit: this.jobsLimit }).then(jobs => { this.jobs = jobs; }); }, } }); </script> <style lang="stylus" scoped> .wptihjuy > .chart min-height 200px !important margin 0 -8px .xvvuvgsv > b margin-right 16px </style>