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/pages/instance | |
| 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/pages/instance')
| -rw-r--r-- | src/client/pages/instance/index.vue | 674 | ||||
| -rw-r--r-- | src/client/pages/instance/monitor.vue | 381 | ||||
| -rw-r--r-- | src/client/pages/instance/settings.vue | 413 | ||||
| -rw-r--r-- | src/client/pages/instance/stats.vue | 491 |
4 files changed, 750 insertions, 1209 deletions
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 5a48232417..db88982330 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -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> - </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> + <mk-instance-stats style="margin-bottom: var(--margin);"/> - <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> + <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> - </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 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="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> + <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"> - <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 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="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> + <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> - <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.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('admin/server-info').then(res => { - this.serverInfo = 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.$root.api('stats').then(res => { - this.stats = res; + 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', + } + } }); - }, - 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 + 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 }); - }; - 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; - chooseProxyAccount() { - this.$root.new(MkUserSelect, {}).$once('selected', user => { - this.proxyAccount = user; - this.proxyAccountId = user.id; - this.save(true); - }); + 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(); }, - 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); + + > 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; - > * { - flex: 1; + > .icon { + margin-right: 4px; + display: none; + } + } + } } } } diff --git a/src/client/pages/instance/monitor.vue b/src/client/pages/instance/monitor.vue deleted file mode 100644 index b75755126b..0000000000 --- a/src/client/pages/instance/monitor.vue +++ /dev/null @@ -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> diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue new file mode 100644 index 0000000000..914565298a --- /dev/null +++ b/src/client/pages/instance/settings.vue @@ -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> diff --git a/src/client/pages/instance/stats.vue b/src/client/pages/instance/stats.vue deleted file mode 100644 index 4883d8c873..0000000000 --- a/src/client/pages/instance/stats.vue +++ /dev/null @@ -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> |