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
This commit is contained in:
@ -40,6 +40,29 @@
|
||||
<span>{{ $t('latest-request-received-at') }}</span>
|
||||
</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>
|
||||
@ -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>
|
||||
|
Reference in New Issue
Block a user