diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b628b91ab0..f353654d17 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -403,6 +403,12 @@ regenerate: "再生成" fontSize: "フォントサイズ" noFollowRequests: "フォロー申請はありません" openImageInNewTab: "画像を新しいタブで開く" +dashboard: "ダッシュボード" +local: "ローカル" +remote: "リモート" +total: "合計" +weekOverWeekChanges: "前週比" +dayOverDayChanges: "前日比" _ago: unknown: "謎" diff --git a/src/client/app.vue b/src/client/app.vue index 1bfcd9e158..9a984a27ff 100644 --- a/src/client/app.vue +++ b/src/client/app.vue @@ -413,9 +413,14 @@ export default Vue.extend({ this.$root.menu({ items: [{ type: 'link', - text: this.$t('statistics'), - to: '/instance/stats', - icon: faChartBar, + text: this.$t('dashboard'), + to: '/instance', + icon: faTachometerAlt, + }, null, { + type: 'link', + text: this.$t('settings'), + to: '/instance/settings', + icon: faCog, }, { type: 'link', text: this.$t('customEmojis'), @@ -431,11 +436,6 @@ export default Vue.extend({ text: this.$t('files'), to: '/instance/files', icon: faCloud, - }, { - type: 'link', - text: this.$t('monitor'), - to: '/instance/monitor', - icon: faTachometerAlt, }, { type: 'link', text: this.$t('jobQueue'), @@ -451,11 +451,6 @@ export default Vue.extend({ text: this.$t('announcements'), to: '/instance/announcements', icon: faBroadcastTower, - }, null, { - type: 'link', - text: this.$t('general'), - to: '/instance', - icon: faCog, }], align: 'left', fixed: true, diff --git a/src/client/pages/instance/stats.vue b/src/client/components/instance-stats.vue similarity index 72% rename from src/client/pages/instance/stats.vue rename to src/client/components/instance-stats.vue index 4883d8c873..a2625f4ab6 100644 --- a/src/client/pages/instance/stats.vue +++ b/src/client/components/instance-stats.vue @@ -1,8 +1,91 @@ <template> -<div class="mk-instance-stats"> +<div class="zbcjwnqg"> + <div class="stats" v-if="info"> + <div class="_panel"> + <div> + <b><fa :icon="faUser"/>{{ $t('users') }}</b> + <small>{{ $t('local') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ info.originalUsersCount | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersLocalDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ usersLocalDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersLocalWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ usersLocalWoW | number }}</dd> + </dl> + </div> + </div> + <div class="_panel"> + <div> + <b><fa :icon="faUser"/>{{ $t('users') }}</b> + <small>{{ $t('remote') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersRemoteDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ usersRemoteDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: usersRemoteWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ usersRemoteWoW | number }}</dd> + </dl> + </div> + </div> + <div class="_panel"> + <div> + <b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> + <small>{{ $t('local') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ info.originalNotesCount | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesLocalDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ notesLocalDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesLocalWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ notesLocalWoW | number }}</dd> + </dl> + </div> + </div> + <div class="_panel"> + <div> + <b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> + <small>{{ $t('remote') }}</small> + </div> + <div> + <dl class="total"> + <dt>{{ $t('total') }}</dt> + <dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesRemoteDoD > 0 }"> + <dt>{{ $t('dayOverDayChanges') }}</dt> + <dd>{{ notesRemoteDoD | number }}</dd> + </dl> + <dl class="diff" :class="{ inc: notesRemoteWoW > 0 }"> + <dt>{{ $t('weekOverWeekChanges') }}</dt> + <dd>{{ notesRemoteWoW | number }}</dd> + </dl> + </div> + </div> + </div> + <section class="_card"> <div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div> - <div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> + <div class="_content" style="margin-top: -8px;"> <div class="selects" style="display: flex;"> <mk-select v-model="chartSrc" style="margin: 0; flex: 1;"> <optgroup :label="$t('federation')"> @@ -40,10 +123,10 @@ <script lang="ts"> import Vue from 'vue'; -import { faChartBar } from '@fortawesome/free-solid-svg-icons'; +import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; import Chart from 'chart.js'; -import i18n from '../../i18n'; -import MkSelect from '../../components/ui/select.vue'; +import i18n from '../i18n'; +import MkSelect from './ui/select.vue'; const chartLimit = 90; const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); @@ -59,24 +142,27 @@ const alpha = (hex, a) => { export default Vue.extend({ i18n, - metaInfo() { - return { - title: `${this.$t('statistics')} | ${this.$t('instance')}` - }; - }, - components: { MkSelect }, data() { return { + info: null, + notesLocalWoW: 0, + notesLocalDoD: 0, + notesRemoteWoW: 0, + notesRemoteDoD: 0, + usersLocalWoW: 0, + usersLocalDoD: 0, + usersRemoteWoW: 0, + usersRemoteDoD: 0, now: null, chart: null, chartInstance: null, chartSrc: 'notes', chartSpan: 'hour', - faChartBar + faChartBar, faUser, faPencilAlt } }, @@ -121,6 +207,8 @@ export default Vue.extend({ }, async created() { + this.info = await this.$root.api('stats'); + this.now = new Date(); const [perHour, perDay] = await Promise.all([Promise.all([ @@ -154,6 +242,15 @@ export default Vue.extend({ } }; + this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7]; + this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1]; + this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7]; + this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1]; + this.usersLocalWoW = this.info.usersCount - chart.perDay.users.local.total[7]; + this.usersLocalDoD = this.info.usersCount - chart.perDay.users.local.total[1]; + this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7]; + this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1]; + this.chart = chart; this.renderChart(); @@ -489,3 +586,80 @@ export default Vue.extend({ } }); </script> + +<style lang="scss" scoped> +.zbcjwnqg { + > .stats { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + margin: calc(0px - var(--margin) / 2); + margin-bottom: calc(var(--margin) / 2); + + > div { + display: flex; + flex: 1 0 213px; + margin: calc(var(--margin) / 2); + box-sizing: border-box; + padding: 16px 20px; + + > div { + width: 50%; + + &:first-child { + > b { + display: block; + + > [data-icon] { + width: 16px; + margin-right: 8px; + } + } + + > small { + margin-left: 16px + 8px; + opacity: 0.7; + } + } + + &:last-child { + > dl { + display: flex; + margin: 0; + line-height: 1.5em; + + > dt, + > dd { + width: 50%; + margin: 0; + } + + > dt { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + &.total { + > dt, + > dd { + font-weight: bold; + } + } + + &.diff.inc { + > dd { + color: #82c11c; + + &:before { + content: "+"; + } + } + } + } + } + } + } + } +} +</style> diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue index e39600b487..a3a4b6ac73 100644 --- a/src/client/pages/about.vue +++ b/src/client/pages/about.vue @@ -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> 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> + <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; + } + } + } } } } 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/router.ts b/src/client/router.ts index a226cc9734..fe3de70a05 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -49,9 +49,8 @@ export const router = new VueRouter({ { path: '/instance/emojis', component: page('instance/emojis') }, { path: '/instance/users', component: page('instance/users') }, { path: '/instance/files', component: page('instance/files') }, - { path: '/instance/monitor', component: page('instance/monitor') }, { path: '/instance/queue', component: page('instance/queue') }, - { path: '/instance/stats', component: page('instance/stats') }, + { path: '/instance/settings', component: page('instance/settings') }, { path: '/instance/federation', component: page('instance/federation') }, { path: '/instance/announcements', component: page('instance/announcements') }, { path: '/notes/:note', name: 'note', component: page('note') },