diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-02-17 02:21:27 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2020-02-17 02:21:27 +0900 |
| commit | f45fb56e15e2925aca192867db0ef4ebb15d1f02 (patch) | |
| tree | 364e1f7dfceb3c37372f943219ab17dc8c6a63a4 /src/client/components/instance-stats.vue | |
| parent | 12.11.0 (diff) | |
| download | sharkey-f45fb56e15e2925aca192867db0ef4ebb15d1f02.tar.gz sharkey-f45fb56e15e2925aca192867db0ef4ebb15d1f02.tar.bz2 sharkey-f45fb56e15e2925aca192867db0ef4ebb15d1f02.zip | |
Improve instance info page
Diffstat (limited to 'src/client/components/instance-stats.vue')
| -rw-r--r-- | src/client/components/instance-stats.vue | 665 |
1 files changed, 665 insertions, 0 deletions
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue new file mode 100644 index 0000000000..a2625f4ab6 --- /dev/null +++ b/src/client/components/instance-stats.vue @@ -0,0 +1,665 @@ +<template> +<div class="zbcjwnqg"> + <div class="stats" v-if="info"> + <div class="_panel"> + <div> + <b><fa :icon="faUser"/>{{ $t('users') }}</b> + <small>{{ $t('local') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ info.originalUsersCount | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersLocalDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ usersLocalDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersLocalWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ usersLocalWoW | number }}</dd> + </dl> + </div> + </div> + <div class="_panel"> + <div> + <b><fa :icon="faUser"/>{{ $t('users') }}</b> + <small>{{ $t('remote') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersRemoteDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ usersRemoteDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersRemoteWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ usersRemoteWoW | number }}</dd> + </dl> + </div> + </div> + <div class="_panel"> + <div> + <b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> + <small>{{ $t('local') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ info.originalNotesCount | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesLocalDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ notesLocalDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesLocalWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ notesLocalWoW | number }}</dd> + </dl> + </div> + </div> + <div class="_panel"> + <div> + <b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> + <small>{{ $t('remote') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesRemoteDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ notesRemoteDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesRemoteWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ notesRemoteWoW | number }}</dd> + </dl> + </div> + </div> + </div> + + <section class="_card"> + <div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div> + <div class="_content" style="margin-top: -8px;"> + <div class="selects" style="display: flex;"> + <mk-select v-model="chartSrc" style="margin: 0; flex: 1;"> + <optgroup :label="$t('federation')"> + <option value="federation-instances">{{ $t('_charts.federationInstancesIncDec') }}</option> + <option value="federation-instances-total">{{ $t('_charts.federationInstancesTotal') }}</option> + </optgroup> + <optgroup :label="$t('users')"> + <option value="users">{{ $t('_charts.usersIncDec') }}</option> + <option value="users-total">{{ $t('_charts.usersTotal') }}</option> + <option value="active-users">{{ $t('_charts.activeUsers') }}</option> + </optgroup> + <optgroup :label="$t('notes')"> + <option value="notes">{{ $t('_charts.notesIncDec') }}</option> + <option value="local-notes">{{ $t('_charts.localNotesIncDec') }}</option> + <option value="remote-notes">{{ $t('_charts.remoteNotesIncDec') }}</option> + <option value="notes-total">{{ $t('_charts.notesTotal') }}</option> + </optgroup> + <optgroup :label="$t('drive')"> + <option value="drive-files">{{ $t('_charts.filesIncDec') }}</option> + <option value="drive-files-total">{{ $t('_charts.filesTotal') }}</option> + <option value="drive">{{ $t('_charts.storageUsageIncDec') }}</option> + <option value="drive-total">{{ $t('_charts.storageUsageTotal') }}</option> + </optgroup> + </mk-select> + <mk-select v-model="chartSpan" style="margin: 0;"> + <option value="hour">{{ $t('perHour') }}</option> + <option value="day">{{ $t('perDay') }}</option> + </mk-select> + </div> + <canvas ref="chart"></canvas> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import Chart from 'chart.js'; +import i18n from '../i18n'; +import MkSelect from './ui/select.vue'; + +const chartLimit = 90; +const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); +const negate = arr => arr.map(x => -x); +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 Vue.extend({ + i18n, + + components: { + MkSelect + }, + + data() { + return { + info: null, + notesLocalWoW: 0, + notesLocalDoD: 0, + notesRemoteWoW: 0, + notesRemoteDoD: 0, + usersLocalWoW: 0, + usersLocalDoD: 0, + usersRemoteWoW: 0, + usersRemoteDoD: 0, + now: null, + chart: null, + chartInstance: null, + chartSrc: 'notes', + chartSpan: 'hour', + faChartBar, faUser, faPencilAlt + } + }, + + computed: { + data(): any { + if (this.chart == null) return null; + switch (this.chartSrc) { + case 'federation-instances': return this.federationInstancesChart(false); + case 'federation-instances-total': return this.federationInstancesChart(true); + case 'users': return this.usersChart(false); + case 'users-total': return this.usersChart(true); + case 'active-users': return this.activeUsersChart(); + case 'notes': return this.notesChart('combined'); + case 'local-notes': return this.notesChart('local'); + case 'remote-notes': return this.notesChart('remote'); + case 'notes-total': return this.notesTotalChart(); + case 'drive': return this.driveChart(); + case 'drive-total': return this.driveTotalChart(); + case 'drive-files': return this.driveFilesChart(); + case 'drive-files-total': return this.driveFilesTotalChart(); + } + }, + + stats(): any[] { + const stats = + this.chartSpan == 'day' ? this.chart.perDay : + this.chartSpan == 'hour' ? this.chart.perHour : + null; + + return stats; + } + }, + + watch: { + chartSrc() { + this.renderChart(); + }, + + chartSpan() { + this.renderChart(); + } + }, + + async created() { + this.info = await this.$root.api('stats'); + + this.now = new Date(); + + const [perHour, perDay] = await Promise.all([Promise.all([ + this.$root.api('charts/federation', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/users', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/active-users', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/notes', { limit: chartLimit, span: 'hour' }), + this.$root.api('charts/drive', { limit: chartLimit, span: 'hour' }), + ]), Promise.all([ + this.$root.api('charts/federation', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/users', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/active-users', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/notes', { limit: chartLimit, span: 'day' }), + this.$root.api('charts/drive', { limit: chartLimit, span: 'day' }), + ])]); + + const chart = { + perHour: { + federation: perHour[0], + users: perHour[1], + activeUsers: perHour[2], + notes: perHour[3], + drive: perHour[4], + }, + perDay: { + federation: perDay[0], + users: perDay[1], + activeUsers: perDay[2], + notes: perDay[3], + drive: perDay[4], + } + }; + + this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7]; + this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1]; + this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7]; + this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1]; + this.usersLocalWoW = this.info.usersCount - chart.perDay.users.local.total[7]; + this.usersLocalDoD = this.info.usersCount - chart.perDay.users.local.total[1]; + this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7]; + this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1]; + + this.chart = chart; + + this.renderChart(); + }, + + methods: { + renderChart() { + if (this.chartInstance) { + this.chartInstance.destroy(); + } + + Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); + this.chartInstance = new Chart(this.$refs.chart, { + type: 'line', + data: { + labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), + datasets: this.data.series.map(x => ({ + label: x.name, + data: x.data.slice().reverse(), + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: x.color, + backgroundColor: alpha(x.color, 0.1), + hidden: !!x.hidden + })) + }, + options: { + aspectRatio: 2.5, + layout: { + padding: { + left: 0, + right: 0, + top: 16, + bottom: 0 + } + }, + legend: { + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + scales: { + xAxes: [{ + gridLines: { + display: false + }, + ticks: { + display: false + } + }], + yAxes: [{ + position: 'right', + ticks: { + display: false + } + }] + }, + tooltips: { + intersect: false, + mode: 'index', + } + } + }); + }, + + getDate(ago: number) { + const y = this.now.getFullYear(); + const m = this.now.getMonth(); + const d = this.now.getDate(); + const h = this.now.getHours(); + + return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); + }, + + format(arr) { + return arr; + }, + + federationInstancesChart(total: boolean): any { + return { + series: [{ + name: 'Instances', + color: '#008FFB', + data: this.format(total + ? this.stats.federation.instance.total + : sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)) + ) + }] + }; + }, + + notesChart(type: string): any { + return { + series: [{ + name: 'All', + type: 'line', + color: '#008FFB', + data: this.format(type == 'combined' + ? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec)) + : sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec)) + ) + }, { + name: 'Renotes', + type: 'area', + color: '#00E396', + data: this.format(type == 'combined' + ? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote) + : this.stats.notes[type].diffs.renote + ) + }, { + name: 'Replies', + type: 'area', + color: '#FEB019', + data: this.format(type == 'combined' + ? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply) + : this.stats.notes[type].diffs.reply + ) + }, { + name: 'Normal', + type: 'area', + color: '#FF4560', + data: this.format(type == 'combined' + ? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal) + : this.stats.notes[type].diffs.normal + ) + }] + }; + }, + + notesTotalChart(): any { + return { + series: [{ + name: 'Combined', + type: 'line', + color: '#008FFB', + data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total)) + }, { + name: 'Local', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.notes.local.total) + }, { + name: 'Remote', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.notes.remote.total) + }] + }; + }, + + usersChart(total: boolean): any { + return { + series: [{ + name: 'Combined', + type: 'line', + color: '#008FFB', + data: this.format(total + ? sum(this.stats.users.local.total, this.stats.users.remote.total) + : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) + ) + }, { + name: 'Local', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(total + ? this.stats.users.local.total + : sum(this.stats.users.local.inc, negate(this.stats.users.local.dec)) + ) + }, { + name: 'Remote', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(total + ? this.stats.users.remote.total + : sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) + ) + }] + }; + }, + + activeUsersChart(): any { + return { + series: [{ + name: 'Combined', + type: 'line', + color: '#008FFB', + data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count)) + }, { + name: 'Local', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.activeUsers.local.count) + }, { + name: 'Remote', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.activeUsers.remote.count) + }] + }; + }, + + driveChart(): any { + return { + bytes: true, + series: [{ + name: 'All', + type: 'line', + color: '#008FFB', + data: this.format( + sum( + this.stats.drive.local.incSize, + negate(this.stats.drive.local.decSize), + this.stats.drive.remote.incSize, + negate(this.stats.drive.remote.decSize) + ) + ) + }, { + name: 'Local +', + type: 'area', + color: '#008FFB', + data: this.format(this.stats.drive.local.incSize) + }, { + name: 'Local -', + type: 'area', + color: '#008FFB', + data: this.format(negate(this.stats.drive.local.decSize)) + }, { + name: 'Remote +', + type: 'area', + color: '#008FFB', + data: this.format(this.stats.drive.remote.incSize) + }, { + name: 'Remote -', + type: 'area', + color: '#008FFB', + data: this.format(negate(this.stats.drive.remote.decSize)) + }] + }; + }, + + driveTotalChart(): any { + return { + bytes: true, + series: [{ + name: 'Combined', + type: 'line', + color: '#008FFB', + data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize)) + }, { + name: 'Local', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.drive.local.totalSize) + }, { + name: 'Remote', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.drive.remote.totalSize) + }] + }; + }, + + driveFilesChart(): any { + return { + series: [{ + name: 'All', + type: 'line', + color: '#008FFB', + data: this.format( + sum( + this.stats.drive.local.incCount, + negate(this.stats.drive.local.decCount), + this.stats.drive.remote.incCount, + negate(this.stats.drive.remote.decCount) + ) + ) + }, { + name: 'Local +', + type: 'area', + color: '#008FFB', + data: this.format(this.stats.drive.local.incCount) + }, { + name: 'Local -', + type: 'area', + color: '#008FFB', + data: this.format(negate(this.stats.drive.local.decCount)) + }, { + name: 'Remote +', + type: 'area', + color: '#008FFB', + data: this.format(this.stats.drive.remote.incCount) + }, { + name: 'Remote -', + type: 'area', + color: '#008FFB', + data: this.format(negate(this.stats.drive.remote.decCount)) + }] + }; + }, + + driveFilesTotalChart(): any { + return { + series: [{ + name: 'Combined', + type: 'line', + color: '#008FFB', + data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount)) + }, { + name: 'Local', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.drive.local.totalCount) + }, { + name: 'Remote', + type: 'area', + color: '#008FFB', + hidden: true, + data: this.format(this.stats.drive.remote.totalCount) + }] + }; + }, + } +}); +</script> + +<style lang="scss" scoped> +.zbcjwnqg { + > .stats { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + margin: calc(0px - var(--margin) / 2); + margin-bottom: calc(var(--margin) / 2); + + > div { + display: flex; + flex: 1 0 213px; + margin: calc(var(--margin) / 2); + box-sizing: border-box; + padding: 16px 20px; + + > div { + width: 50%; + + &:first-child { + > b { + display: block; + + > [data-icon] { + width: 16px; + margin-right: 8px; + } + } + + > small { + margin-left: 16px + 8px; + opacity: 0.7; + } + } + + &:last-child { + > dl { + display: flex; + margin: 0; + line-height: 1.5em; + + > dt, + > dd { + width: 50%; + margin: 0; + } + + > dt { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + &.total { + > dt, + > dd { + font-weight: bold; + } + } + + &.diff.inc { + > dd { + color: #82c11c; + + &:before { + content: "+"; + } + } + } + } + } + } + } + } +} +</style> |