diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2019-02-08 16:58:57 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-02-08 16:58:57 +0900 |
| commit | 56275bcfcbd1366b0e96b79915e810baed0548bb (patch) | |
| tree | b35c8a6bd15ef8046c00119f0271808970f4f939 /src/client/app/admin | |
| parent | Supress logs during test (diff) | |
| download | sharkey-56275bcfcbd1366b0e96b79915e810baed0548bb.tar.gz sharkey-56275bcfcbd1366b0e96b79915e810baed0548bb.tar.bz2 sharkey-56275bcfcbd1366b0e96b79915e810baed0548bb.zip | |
Introduce per-instance chart (#4183)
* Introduce per-instance chart
* Implement chart view in client
* Handle note deleting
* More chart srcs
* Add drive stats
* Improve drive stats
* Fix bug
* Add icon
Diffstat (limited to 'src/client/app/admin')
| -rw-r--r-- | src/client/app/admin/views/federation.vue | 275 |
1 files changed, 272 insertions, 3 deletions
diff --git a/src/client/app/admin/views/federation.vue b/src/client/app/admin/views/federation.vue index dd8567243a..8b0e9ba45a 100644 --- a/src/client/app/admin/views/federation.vue +++ b/src/client/app/admin/views/federation.vue @@ -41,6 +41,29 @@ </ui-input> <ui-switch v-model="instance.isBlocked" @change="updateInstance()">{{ $t('block') }}</ui-switch> <details> + <summary>{{ $t('charts') }}</summary> + <ui-horizon-group inputs> + <ui-select v-model="chartSrc"> + <option value="requests">{{ $t('chart-srcs.requests') }}</option> + <option value="users">{{ $t('chart-srcs.users') }}</option> + <option value="users-total">{{ $t('chart-srcs.users-total') }}</option> + <option value="notes">{{ $t('chart-srcs.notes') }}</option> + <option value="notes-total">{{ $t('chart-srcs.notes-total') }}</option> + <option value="ff">{{ $t('chart-srcs.ff') }}</option> + <option value="ff-total">{{ $t('chart-srcs.ff-total') }}</option> + <option value="drive-usage">{{ $t('chart-srcs.drive-usage') }}</option> + <option value="drive-usage-total">{{ $t('chart-srcs.drive-usage-total') }}</option> + <option value="drive-files">{{ $t('chart-srcs.drive-files') }}</option> + <option value="drive-files-total">{{ $t('chart-srcs.drive-files-total') }}</option> + </ui-select> + <ui-select v-model="chartSpan"> + <option value="hour">{{ $t('chart-spans.hour') }}</option> + <option value="day">{{ $t('chart-spans.day') }}</option> + </ui-select> + </ui-horizon-group> + <div ref="chart"></div> + </details> + <details> <summary>{{ $t('remove-all-following') }}</summary> <ui-button @click="removeAllFollowing()" style="margin-top: 16px;"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button> <ui-info warn>{{ $t('remove-all-following-info', { host: instance.host }) }}</ui-info> @@ -50,7 +73,7 @@ </ui-card> <ui-card> - <div slot="title"><fa :icon="faUsers"/> {{ $t('instances') }}</div> + <div slot="title"><fa :icon="faServer"/> {{ $t('instances') }}</div> <section class="fit-top"> <ui-horizon-group inputs> <ui-select v-model="sort"> @@ -65,6 +88,10 @@ <option value="+following">{{ $t('sorts.followingDesc') }}</option> <option value="-followers">{{ $t('sorts.followersAsc') }}</option> <option value="+followers">{{ $t('sorts.followersDesc') }}</option> + <option value="-driveUsage">{{ $t('sorts.driveUsageAsc') }}</option> + <option value="+driveUsage">{{ $t('sorts.driveUsageDesc') }}</option> + <option value="-driveFiles">{{ $t('sorts.driveFilesAsc') }}</option> + <option value="+driveFiles">{{ $t('sorts.driveFilesDesc') }}</option> </ui-select> <ui-select v-model="state"> <span slot="label">{{ $t('state') }}</span> @@ -101,7 +128,13 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../i18n'; -import { faGlobe, faTerminal, faSearch, faMinusCircle } from '@fortawesome/free-solid-svg-icons'; +import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer } from '@fortawesome/free-solid-svg-icons'; +import ApexCharts from 'apexcharts'; +import * as tinycolor from 'tinycolor2'; + +const chartLimit = 90; +const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); +const negate = arr => arr.map(x => -x); export default Vue.extend({ i18n: i18n('admin/views/federation.vue'), @@ -114,10 +147,42 @@ export default Vue.extend({ state: 'all', limit: 50, instances: [], - faGlobe, faTerminal, faSearch, faMinusCircle + chart: null, + chartSrc: 'requests', + chartSpan: 'hour', + chartInstance: null, + faGlobe, faTerminal, faSearch, faMinusCircle, faServer }; }, + computed: { + data(): any { + if (this.chart == null) return null; + switch (this.chartSrc) { + case 'requests': return this.requestsChart(); + case 'users': return this.usersChart(false); + case 'users-total': return this.usersChart(true); + case 'notes': return this.notesChart(false); + case 'notes-total': return this.notesChart(true); + case 'ff': return this.ffChart(false); + case 'ff-total': return this.ffChart(true); + case 'drive-usage': return this.driveUsageChart(false); + case 'drive-usage-total': return this.driveUsageChart(true); + case 'drive-files': return this.driveFilesChart(false); + case 'drive-files-total': return this.driveFilesChart(true); + } + }, + + stats(): any[] { + const stats = + this.chartSpan == 'day' ? this.chart.perDay : + this.chartSpan == 'hour' ? this.chart.perHour : + null; + + return stats; + } + }, + watch: { sort() { this.fetchInstances(); @@ -126,12 +191,42 @@ export default Vue.extend({ state() { this.fetchInstances(); }, + + async instance() { + this.now = new Date(); + + const [perHour, perDay] = await Promise.all([ + this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), + this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), + ]); + + const chart = { + perHour: perHour, + perDay: perDay + }; + + this.chart = chart; + + this.renderChart(); + }, + + chartSrc() { + this.renderChart(); + }, + + chartSpan() { + this.renderChart(); + } }, mounted() { this.fetchInstances(); }, + beforeDestroy() { + this.chartInstance.destroy(); + }, + methods: { showInstance() { this.$root.api('federation/show-instance', { @@ -177,6 +272,180 @@ export default Vue.extend({ isBlocked: this.instance.isBlocked, }); }, + + setSrc(src) { + this.chartSrc = src; + }, + + renderChart() { + if (this.chartInstance) { + this.chartInstance.destroy(); + } + + this.chartInstance = new ApexCharts(this.$refs.chart, { + chart: { + type: 'area', + height: 300, + animations: { + dynamicAnimation: { + enabled: false + } + }, + toolbar: { + show: false + }, + zoom: { + enabled: false + } + }, + dataLabels: { + enabled: false + }, + grid: { + clipMarkers: false, + borderColor: 'rgba(0, 0, 0, 0.1)' + }, + stroke: { + curve: 'straight', + width: 2 + }, + legend: { + labels: { + colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() + }, + }, + xaxis: { + type: 'datetime', + labels: { + style: { + colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() + } + }, + axisBorder: { + color: 'rgba(0, 0, 0, 0.1)' + }, + axisTicks: { + color: 'rgba(0, 0, 0, 0.1)' + }, + }, + yaxis: { + labels: { + formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v), + style: { + color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString() + } + } + }, + series: this.data.series + }); + + this.chartInstance.render(); + }, + + getDate(i: 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 - i) : + this.chartSpan == 'hour' ? new Date(y, m, d, h - i) : + null + ); + }, + + format(arr) { + return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v })); + }, + + requestsChart(): any { + return { + series: [{ + name: 'Incoming', + data: this.format(this.stats.requests.received) + }, { + name: 'Outgoing (succeeded)', + data: this.format(this.stats.requests.succeeded) + }, { + name: 'Outgoing (failed)', + data: this.format(this.stats.requests.failed) + }] + }; + }, + + usersChart(total: boolean): any { + return { + series: [{ + name: 'Users', + type: 'area', + data: this.format(total + ? this.stats.users.total + : sum(this.stats.users.inc, negate(this.stats.users.dec)) + ) + }] + }; + }, + + notesChart(total: boolean): any { + return { + series: [{ + name: 'Notes', + type: 'area', + data: this.format(total + ? this.stats.notes.total + : sum(this.stats.notes.inc, negate(this.stats.notes.dec)) + ) + }] + }; + }, + + ffChart(total: boolean): any { + return { + series: [{ + name: 'Following', + type: 'area', + data: this.format(total + ? this.stats.following.total + : sum(this.stats.following.inc, negate(this.stats.following.dec)) + ) + }, { + name: 'Followers', + type: 'area', + data: this.format(total + ? this.stats.followers.total + : sum(this.stats.followers.inc, negate(this.stats.followers.dec)) + ) + }] + }; + }, + + driveUsageChart(total: boolean): any { + return { + bytes: true, + series: [{ + name: 'Drive usage', + type: 'area', + data: this.format(total + ? this.stats.drive.totalUsage + : sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) + ) + }] + }; + }, + + driveFilesChart(total: boolean): any { + return { + series: [{ + name: 'Drive files', + type: 'area', + data: this.format(total + ? this.stats.drive.totalFiles + : sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) + ) + }] + }; + }, } }); </script> |