Improve instance info page

This commit is contained in:
syuilo 2020-02-17 02:21:27 +09:00
parent 8fe153c7c1
commit f45fb56e15
8 changed files with 962 additions and 760 deletions

View file

@ -12,14 +12,12 @@
<div><b>{{ $t('administrator') }}</b><span>{{ meta.maintainerName }}</span></div>
<div><b></b><span>{{ meta.maintainerEmail }}</span></div>
</div>
<div class="_content table" v-if="stats">
<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
</div>
<div class="_content table">
<div><b>Misskey</b><span>v{{ version }}</span></div>
</div>
</section>
<mk-instance-stats style="margin-top: var(--margin);"/>
</div>
</template>
@ -28,6 +26,7 @@ import Vue from 'vue';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { version } from '../config';
import i18n from '../i18n';
import MkInstanceStats from '../components/instance-stats.vue';
export default Vue.extend({
i18n,
@ -38,10 +37,13 @@ export default Vue.extend({
};
},
components: {
MkInstanceStats
},
data() {
return {
version,
stats: null,
serverInfo: null,
faInfoCircle
}
@ -52,12 +54,6 @@ export default Vue.extend({
return this.$store.state.instance.meta;
},
},
created() {
this.$root.api('stats').then(res => {
this.stats = res;
});
},
});
</script>

View file

@ -1,169 +1,54 @@
<template>
<div v-if="meta" class="mk-instance-page">
<div v-if="meta" class="xhexznfu">
<portal to="icon"><fa :icon="faServer"/></portal>
<portal to="title">{{ $t('instance') }}</portal>
<section class="_card info">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<div class="_content">
<mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
<mk-instance-stats style="margin-bottom: var(--margin);"/>
<section class="_card chart">
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="cpumem"></canvas>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
</div>
<div class="row">
<div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
<div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card info">
<div class="_content">
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
<section class="_card chart">
<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="disk"></canvas>
</div>
<div class="_content">
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
<div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card info">
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
<section class="_card chart">
<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="net"></canvas>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
<template v-if="enableRecaptcha">
<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
</template>
</div>
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
<header>{{ $t('preview') }}</header>
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
<template v-if="enableServiceWorker">
<mk-horizon-group inputs class="fit-bottom">
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
</mk-horizon-group>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<mk-textarea v-model="pinnedUsers">
<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<mk-textarea v-model="blockedHosts">
<template #desc>{{ $t('blockedInstancesDescription') }}</template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><fa :icon="faTwitter"/> Twitter</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableTwitterIntegration">
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faGithub"/> GitHub</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableGithubIntegration">
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faDiscord"/> Discord</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableDiscordIntegration">
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card info">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
<div class="_content table" v-if="stats">
<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
</div>
<div class="_content table">
<div><b>Misskey</b><span>v{{ version }}</span></div>
</div>
<div class="_content table" v-if="serverInfo">
<div><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
<div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
<div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
</div>
</div>
</div>
</section>
</div>
@ -171,18 +56,19 @@
<script lang="ts">
import Vue from 'vue';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import MkUserSelect from '../../components/user-select.vue';
import { faServer, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
import MkInstanceStats from '../../components/instance-stats.vue';
import { version, url } from '../../config';
import i18n from '../../i18n';
import getAcct from '../../../misc/acct/render';
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,
@ -194,11 +80,7 @@ export default Vue.extend({
},
components: {
MkButton,
MkInput,
MkTextarea,
MkSwitch,
MkInfo,
MkInstanceStats,
},
data() {
@ -207,41 +89,11 @@ export default Vue.extend({
url,
stats: null,
serverInfo: null,
proxyAccount: null,
proxyAccountId: null,
cacheRemoteFiles: false,
proxyRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
blockedHosts: '',
pinnedUsers: '',
maintainerName: null,
maintainerEmail: null,
name: null,
description: null,
tosUrl: null,
bannerUrl: null,
iconUrl: null,
maxNoteTextLength: 0,
enableRegistration: false,
enableLocalTimeline: false,
enableGlobalTimeline: false,
enableRecaptcha: false,
recaptchaSiteKey: null,
recaptchaSecretKey: null,
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
enableGithubIntegration: false,
githubClientId: null,
githubClientSecret: null,
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
connection: null,
memUsage: 0,
chartCpuMem: null,
chartNet: null,
faServer, faExchangeAlt, faMicrochip, faHdd
}
},
@ -251,160 +103,308 @@ export default Vue.extend({
},
},
created() {
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
this.maintainerName = this.meta.maintainerName;
this.maintainerEmail = this.meta.maintainerEmail;
this.maxNoteTextLength = this.meta.maxNoteTextLength;
this.enableRegistration = !this.meta.disableRegistration;
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
this.enableRecaptcha = this.meta.enableRecaptcha;
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
this.proxyAccountId = this.meta.proxyAccountId;
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
this.githubClientId = this.meta.githubClientId;
this.githubClientSecret = this.meta.githubClientSecret;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret;
mounted() {
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
this.proxyAccount = proxyAccount;
});
}
this.$root.api('admin/server-info').then(res => {
this.serverInfo = res;
this.chartCpuMem = new Chart(this.$refs.cpumem, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#86b300',
backgroundColor: alpha('#86b300', 0.1),
data: []
}, {
label: 'MEM (active)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
data: []
}, {
label: 'MEM (used)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
max: 100
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.$root.api('stats').then(res => {
this.stats = res;
this.chartNet = new Chart(this.$refs.net, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'In',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Out',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
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',
}
}
});
this.chartDisk = new Chart(this.$refs.disk, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Read',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Write',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
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',
}
}
});
this.$root.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
this.connection = this.$root.stream.useSharedConnection('serverStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 150
});
});
},
mounted() {
const renderRecaptchaPreview = () => {
if (!(window as any).grecaptcha) return;
if (!this.$refs.recaptcha) return;
if (!this.recaptchaSiteKey) return;
(window as any).grecaptcha.render(this.$refs.recaptcha, {
sitekey: this.recaptchaSiteKey
});
};
window.onRecaotchaLoad = () => {
renderRecaptchaPreview();
};
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
head.appendChild(script);
this.$watch('enableRecaptcha', () => {
renderRecaptchaPreview();
});
this.$watch('recaptchaSiteKey', () => {
renderRecaptchaPreview();
});
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
this.connection.dispose();
},
methods: {
addPinUser() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.pinnedUsers = this.pinnedUsers.trim();
this.pinnedUsers += '\n@' + getAcct(user);
this.pinnedUsers = this.pinnedUsers.trim();
});
onStats(stats) {
const cpu = (stats.cpu * 100).toFixed(0);
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
this.memUsage = stats.mem.active;
this.chartCpuMem.data.labels.push('');
this.chartCpuMem.data.datasets[0].data.push(cpu);
this.chartCpuMem.data.datasets[1].data.push(memActive);
this.chartCpuMem.data.datasets[2].data.push(memUsed);
this.chartNet.data.labels.push('');
this.chartNet.data.datasets[0].data.push(stats.net.rx);
this.chartNet.data.datasets[1].data.push(stats.net.tx);
this.chartDisk.data.labels.push('');
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
this.chartCpuMem.data.labels.shift();
this.chartCpuMem.data.datasets[0].data.shift();
this.chartCpuMem.data.datasets[1].data.shift();
this.chartCpuMem.data.datasets[2].data.shift();
this.chartNet.data.labels.shift();
this.chartNet.data.datasets[0].data.shift();
this.chartNet.data.datasets[1].data.shift();
this.chartDisk.data.labels.shift();
this.chartDisk.data.datasets[0].data.shift();
this.chartDisk.data.datasets[1].data.shift();
}
this.chartCpuMem.update();
this.chartNet.update();
this.chartDisk.update();
},
chooseProxyAccount() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save(true);
});
},
save(withDialog = false) {
this.$root.api('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
disableRegistration: !this.enableRegistration,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
enableRecaptcha: this.enableRecaptcha,
recaptchaSiteKey: this.recaptchaSiteKey,
recaptchaSecretKey: this.recaptchaSecretKey,
proxyAccountId: this.proxyAccountId,
cacheRemoteFiles: this.cacheRemoteFiles,
proxyRemoteFiles: this.proxyRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
blockedHosts: this.blockedHosts.split('\n') || [],
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,
enableGithubIntegration: this.enableGithubIntegration,
githubClientId: this.githubClientId,
githubClientSecret: this.githubClientSecret,
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
this.$store.dispatch('instance/fetch');
if (withDialog) {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="scss" scoped>
.mk-instance-page {
> .info {
> .table {
> div {
display: flex;
.xhexznfu {
> .stats {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin: calc(0px - var(--margin) / 2);
margin-bottom: calc(var(--margin) / 2);
> * {
flex: 1;
> div {
flex: 1 0 213px;
margin: calc(var(--margin) / 2);
box-sizing: border-box;
padding: 16px;
}
}
> .chart {
> ._content {
> .table {
> .row {
display: flex;
&:not(:last-child) {
margin-bottom: 16px;
@media (max-width: 500px) {
margin-bottom: 8px;
}
}
> .cell {
flex: 1;
> .label {
font-size: 80%;
opacity: 0.7;
> .icon {
margin-right: 4px;
display: none;
}
}
}
}
}
}

View file

@ -1,381 +0,0 @@
<template>
<div class="mk-instance-monitor">
<section class="_card">
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="cpumem"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
</div>
<div class="row">
<div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
<div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="disk"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
<div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="net"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faTachometerAlt, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
import i18n from '../../i18n';
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,
metaInfo() {
return {
title: `${this.$t('monitor')} | ${this.$t('instance')}`
};
},
components: {
},
data() {
return {
connection: null,
serverInfo: null,
memUsage: 0,
chartCpuMem: null,
chartNet: null,
faTachometerAlt, faExchangeAlt, faMicrochip, faHdd
}
},
mounted() {
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chartCpuMem = new Chart(this.$refs.cpumem, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#86b300',
backgroundColor: alpha('#86b300', 0.1),
data: []
}, {
label: 'MEM (active)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
data: []
}, {
label: 'MEM (used)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
max: 100
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.chartNet = new Chart(this.$refs.net, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'In',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Out',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
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',
}
}
});
this.chartDisk = new Chart(this.$refs.disk, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Read',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Write',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
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',
}
}
});
this.$root.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
this.connection = this.$root.stream.useSharedConnection('serverStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 150
});
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
this.connection.dispose();
},
methods: {
onStats(stats) {
const cpu = (stats.cpu * 100).toFixed(0);
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
this.memUsage = stats.mem.active;
this.chartCpuMem.data.labels.push('');
this.chartCpuMem.data.datasets[0].data.push(cpu);
this.chartCpuMem.data.datasets[1].data.push(memActive);
this.chartCpuMem.data.datasets[2].data.push(memUsed);
this.chartNet.data.labels.push('');
this.chartNet.data.datasets[0].data.push(stats.net.rx);
this.chartNet.data.datasets[1].data.push(stats.net.tx);
this.chartDisk.data.labels.push('');
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
this.chartCpuMem.data.labels.shift();
this.chartCpuMem.data.datasets[0].data.shift();
this.chartCpuMem.data.datasets[1].data.shift();
this.chartCpuMem.data.datasets[2].data.shift();
this.chartNet.data.labels.shift();
this.chartNet.data.datasets[0].data.shift();
this.chartNet.data.datasets[1].data.shift();
this.chartDisk.data.labels.shift();
this.chartDisk.data.datasets[0].data.shift();
this.chartDisk.data.datasets[1].data.shift();
}
this.chartCpuMem.update();
this.chartNet.update();
this.chartDisk.update();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="scss" scoped>
.mk-instance-monitor {
> section {
> ._content {
> .table {
> .row {
display: flex;
&:not(:last-child) {
margin-bottom: 16px;
@media (max-width: 500px) {
margin-bottom: 8px;
}
}
> .cell {
flex: 1;
> .label {
font-size: 80%;
opacity: 0.7;
> .icon {
margin-right: 4px;
display: none;
}
}
}
}
}
}
}
}
</style>

View file

@ -0,0 +1,413 @@
<template>
<div v-if="meta" class="yihovjtf">
<portal to="icon"><fa :icon="faCog"/></portal>
<portal to="title">{{ $t('settings') }}</portal>
<section class="_card info">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<div class="_content">
<mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card info">
<div class="_content">
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
</div>
<div class="_content">
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
</div>
</section>
<section class="_card info">
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
<template v-if="enableRecaptcha">
<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
</template>
</div>
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
<header>{{ $t('preview') }}</header>
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
<template v-if="enableServiceWorker">
<mk-horizon-group inputs class="fit-bottom">
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
</mk-horizon-group>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<mk-textarea v-model="pinnedUsers">
<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<mk-textarea v-model="blockedHosts">
<template #desc>{{ $t('blockedInstancesDescription') }}</template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><fa :icon="faTwitter"/> Twitter</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableTwitterIntegration">
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faGithub"/> GitHub</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableGithubIntegration">
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faDiscord"/> Discord</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableDiscordIntegration">
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card info">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
<div class="_content table" v-if="stats">
<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
</div>
<div class="_content table">
<div><b>Misskey</b><span>v{{ version }}</span></div>
</div>
<div class="_content table" v-if="serverInfo">
<div><b>Node.js</b><span>{{ serverInfo.node }}</span></div>
<div><b>PostgreSQL</b><span>v{{ serverInfo.psql }}</span></div>
<div><b>Redis</b><span>v{{ serverInfo.redis }}</span></div>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import MkUserSelect from '../../components/user-select.vue';
import { version, url } from '../../config';
import i18n from '../../i18n';
import getAcct from '../../../misc/acct/render';
export default Vue.extend({
i18n,
metaInfo() {
return {
title: this.$t('instance') as string
};
},
components: {
MkButton,
MkInput,
MkTextarea,
MkSwitch,
MkInfo,
},
data() {
return {
version,
url,
stats: null,
serverInfo: null,
proxyAccount: null,
proxyAccountId: null,
cacheRemoteFiles: false,
proxyRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
blockedHosts: '',
pinnedUsers: '',
maintainerName: null,
maintainerEmail: null,
name: null,
description: null,
tosUrl: null,
bannerUrl: null,
iconUrl: null,
maxNoteTextLength: 0,
enableRegistration: false,
enableLocalTimeline: false,
enableGlobalTimeline: false,
enableRecaptcha: false,
recaptchaSiteKey: null,
recaptchaSecretKey: null,
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
enableGithubIntegration: false,
githubClientId: null,
githubClientSecret: null,
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
this.maintainerName = this.meta.maintainerName;
this.maintainerEmail = this.meta.maintainerEmail;
this.maxNoteTextLength = this.meta.maxNoteTextLength;
this.enableRegistration = !this.meta.disableRegistration;
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
this.enableRecaptcha = this.meta.enableRecaptcha;
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
this.proxyAccountId = this.meta.proxyAccountId;
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
this.githubClientId = this.meta.githubClientId;
this.githubClientSecret = this.meta.githubClientSecret;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret;
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
this.proxyAccount = proxyAccount;
});
}
this.$root.api('admin/server-info').then(res => {
this.serverInfo = res;
});
this.$root.api('stats').then(res => {
this.stats = res;
});
},
mounted() {
const renderRecaptchaPreview = () => {
if (!(window as any).grecaptcha) return;
if (!this.$refs.recaptcha) return;
if (!this.recaptchaSiteKey) return;
(window as any).grecaptcha.render(this.$refs.recaptcha, {
sitekey: this.recaptchaSiteKey
});
};
window.onRecaotchaLoad = () => {
renderRecaptchaPreview();
};
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
head.appendChild(script);
this.$watch('enableRecaptcha', () => {
renderRecaptchaPreview();
});
this.$watch('recaptchaSiteKey', () => {
renderRecaptchaPreview();
});
},
methods: {
addPinUser() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.pinnedUsers = this.pinnedUsers.trim();
this.pinnedUsers += '\n@' + getAcct(user);
this.pinnedUsers = this.pinnedUsers.trim();
});
},
chooseProxyAccount() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save(true);
});
},
save(withDialog = false) {
this.$root.api('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
disableRegistration: !this.enableRegistration,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
enableRecaptcha: this.enableRecaptcha,
recaptchaSiteKey: this.recaptchaSiteKey,
recaptchaSecretKey: this.recaptchaSecretKey,
proxyAccountId: this.proxyAccountId,
cacheRemoteFiles: this.cacheRemoteFiles,
proxyRemoteFiles: this.proxyRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
blockedHosts: this.blockedHosts.split('\n') || [],
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,
enableGithubIntegration: this.enableGithubIntegration,
githubClientId: this.githubClientId,
githubClientSecret: this.githubClientSecret,
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
this.$store.dispatch('instance/fetch');
if (withDialog) {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
}
}
});
</script>
<style lang="scss" scoped>
.yihovjtf {
> .info {
> .table {
> div {
display: flex;
> * {
flex: 1;
}
}
}
}
}
</style>

View file

@ -1,491 +0,0 @@
<template>
<div class="mk-instance-stats">
<section class="_card">
<div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<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 } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
import i18n from '../../i18n';
import MkSelect from '../../components/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,
metaInfo() {
return {
title: `${this.$t('statistics')} | ${this.$t('instance')}`
};
},
components: {
MkSelect
},
data() {
return {
now: null,
chart: null,
chartInstance: null,
chartSrc: 'notes',
chartSpan: 'hour',
faChartBar
}
},
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.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.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>