diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2018-11-02 23:05:53 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2018-11-02 23:05:53 +0900 |
| commit | f2e719b3612506493b0d27c4476fcd0879ed1eea (patch) | |
| tree | 6d3af4d1fd886ea963f6a67821d669b4b1cd53fe /src/client/app/admin | |
| parent | [API] Implement admin/add-emoji (diff) | |
| download | sharkey-f2e719b3612506493b0d27c4476fcd0879ed1eea.tar.gz sharkey-f2e719b3612506493b0d27c4476fcd0879ed1eea.tar.bz2 sharkey-f2e719b3612506493b0d27c4476fcd0879ed1eea.zip | |
[Client] Admin page improved
Diffstat (limited to 'src/client/app/admin')
| -rw-r--r-- | src/client/app/admin/script.ts | 27 | ||||
| -rw-r--r-- | src/client/app/admin/style.styl | 6 | ||||
| -rw-r--r-- | src/client/app/admin/views/announcements.vue | 55 | ||||
| -rw-r--r-- | src/client/app/admin/views/cpu-memory.vue | 137 | ||||
| -rw-r--r-- | src/client/app/admin/views/dashboard.vue | 117 | ||||
| -rw-r--r-- | src/client/app/admin/views/emoji.vue | 48 | ||||
| -rw-r--r-- | src/client/app/admin/views/hashtags.vue | 46 | ||||
| -rw-r--r-- | src/client/app/admin/views/index.vue | 101 | ||||
| -rw-r--r-- | src/client/app/admin/views/instance.vue | 62 | ||||
| -rw-r--r-- | src/client/app/admin/views/users.vue | 129 |
10 files changed, 728 insertions, 0 deletions
diff --git a/src/client/app/admin/script.ts b/src/client/app/admin/script.ts new file mode 100644 index 0000000000..4002734d3d --- /dev/null +++ b/src/client/app/admin/script.ts @@ -0,0 +1,27 @@ +/** + * Admin + */ + +import VueRouter from 'vue-router'; + +// Style +import './style.styl'; + +import init from '../init'; +import Index from './views/index.vue'; + +init(launch => { + document.title = 'Admin'; + + // Init router + const router = new VueRouter({ + mode: 'history', + base: '/admin/', + routes: [ + { path: '/', component: Index }, + ] + }); + + // Launch the app + launch(router); +}); diff --git a/src/client/app/admin/style.styl b/src/client/app/admin/style.styl new file mode 100644 index 0000000000..5ea3950464 --- /dev/null +++ b/src/client/app/admin/style.styl @@ -0,0 +1,6 @@ +@import "../app" +@import "../reset" + +html + height 100% + background #EBEBEB diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue new file mode 100644 index 0000000000..049a8d5721 --- /dev/null +++ b/src/client/app/admin/views/announcements.vue @@ -0,0 +1,55 @@ +<template> +<div> + <ui-card> + <div slot="title">%i18n:@announcements%</div> + <section> + <textarea class="qldxjjsrseehkusjuoooapmsprvfrxyl" v-model="broadcasts" placeholder='[ { "title": "Title1", "text": "Text1" }, { "title": "Title2", "text": "Text2" } ]'></textarea> + <ui-button @click="save">%i18n:@save%</ui-button> + </section> + </ui-card> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; + +export default Vue.extend({ + data() { + return { + broadcasts: '', + }; + }, + created() { + (this as any).os.getMeta().then(meta => { + this.broadcasts = JSON.stringify(meta.broadcasts, null, ' '); + }); + }, + methods: { + save() { + let json; + + try { + json = JSON.parse(this.broadcasts); + } catch (e) { + (this as any).os.apis.dialog({ text: `Failed: ${e}` }); + return; + } + + (this as any).api('admin/update-meta', { + broadcasts: json + }).then(() => { + (this as any).os.apis.dialog({ text: `Saved` }); + }.catch(e => { + (this as any).os.apis.dialog({ text: `Failed ${e}` }); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.qldxjjsrseehkusjuoooapmsprvfrxyl + width 100% + min-height 300px + +</style> diff --git a/src/client/app/admin/views/cpu-memory.vue b/src/client/app/admin/views/cpu-memory.vue new file mode 100644 index 0000000000..5d03b30ef4 --- /dev/null +++ b/src/client/app/admin/views/cpu-memory.vue @@ -0,0 +1,137 @@ +<template> +<div class="zyknedwtlthezamcjlolyusmipqmjgxz"> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <defs> + <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="cpuPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="cpuPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="1"/> + </mask> + </defs> + <rect + x="0" y="0" + :width="viewBoxX" :height="viewBoxY" + :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/> + <text x="1" y="12">CPU <tspan>{{ cpuP }}%</tspan></text> + </svg> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <defs> + <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="memPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="memPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="1"/> + </mask> + </defs> + <rect + x="0" y="0" + :width="viewBoxX" :height="viewBoxY" + :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/> + <text x="1" y="12">MEM <tspan>{{ memP }}%</tspan></text> + </svg> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + props: ['connection'], + data() { + return { + viewBoxX: 200, + viewBoxY: 70, + stats: [], + cpuGradientId: uuid(), + cpuMaskId: uuid(), + memGradientId: uuid(), + memMaskId: uuid(), + cpuPolylinePoints: '', + memPolylinePoints: '', + cpuPolygonPoints: '', + memPolygonPoints: '', + cpuP: '', + memP: '' + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + this.connection.on('statsLog', this.onStatsLog); + this.connection.send('requestLog', { + id: Math.random().toString().substr(2, 8), + length: 200 + }); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + this.connection.off('statsLog', this.onStatsLog); + }, + methods: { + onStats(stats) { + this.stats.push(stats); + if (this.stats.length > 200) this.stats.shift(); + + const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]); + const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]); + this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); + this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); + + this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; + this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; + + this.cpuP = (stats.cpu_usage * 100).toFixed(0); + this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); + }, + onStatsLog(statsLog) { + statsLog.reverse().forEach(stats => this.onStats(stats)); + } + } +}); +</script> + +<style lang="stylus" scoped> +.zyknedwtlthezamcjlolyusmipqmjgxz + > svg + display block + width 50% + float left + + &:first-child + padding-right 5px + + &:last-child + padding-left 5px + + > text + font-size 10px + fill var(--chartCaption) + + > tspan + opacity 0.5 + + &:after + content "" + display block + clear both + +</style> diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue new file mode 100644 index 0000000000..5af5255e20 --- /dev/null +++ b/src/client/app/admin/views/dashboard.vue @@ -0,0 +1,117 @@ +<template> +<div class="obdskegsannmntldydackcpzezagxqfy"> + <div v-if="stats" class="stats"> + <div> + <div>%fa:user%</div> + <div> + <span>%i18n:@original-users%</span> + <b>{{ stats.originalUsersCount | number }}</b> + </div> + </div> + <div> + <div>%fa:pencil-alt%</div> + <div> + <span>%i18n:@original-notes%</span> + <b>{{ stats.originalNotesCount | number }}</b> + </div> + </div> + <div> + <div>%fa:user%</div> + <div> + <span>%i18n:@all-users%</span> + <b>{{ stats.usersCount | number }}</b> + </div> + </div> + <div> + <div>%fa:pencil-alt%</div> + <div> + <span>%i18n:@all-notes%</span> + <b>{{ stats.notesCount | number }}</b> + </div> + </div> + </div> + + <div class="cpu-memory"> + <x-cpu-memory :connection="connection"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import XCpuMemory from "./cpu-memory.vue"; + +export default Vue.extend({ + components: { + XCpuMemory + }, + data() { + return { + stats: null, + connection: null + }; + }, + created() { + this.connection = (this as any).os.stream.useSharedConnection('serverStats'); + + (this as any).os.getMeta().then(meta => { + this.disableRegistration = meta.disableRegistration; + this.disableLocalTimeline = meta.disableLocalTimeline; + this.bannerUrl = meta.bannerUrl; + }); + + (this as any).api('stats').then(stats => { + this.stats = stats; + }); + }, + beforeDestroy() { + this.connection.dispose(); + } +}); +</script> + +<style lang="stylus" scoped> +.obdskegsannmntldydackcpzezagxqfy + > .stats + display flex + justify-content space-between + margin-bottom 16px + + > div + display flex + align-items center + flex 1 + max-width 300px + margin-right 16px + text-align center + color var(--text) + box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) + background var(--face) + border-radius 8px + + &:last-child + margin-right 0 + + > div:first-child + padding 16px 24px + font-size 28px + + > div:last-child + flex 1 + padding 16px 32px 16px 0 + text-align right + + > span + opacity 0.7 + + > b + display block + + > .cpu-memory + margin-bottom 16px + padding 32px + box-shadow 0 2px 4px rgba(0, 0, 0, 0.1) + background var(--face) + border-radius 8px + +</style> diff --git a/src/client/app/admin/views/emoji.vue b/src/client/app/admin/views/emoji.vue new file mode 100644 index 0000000000..1446cc1a91 --- /dev/null +++ b/src/client/app/admin/views/emoji.vue @@ -0,0 +1,48 @@ +<template> +<div> + <ui-card> + <div slot="title">%fa:plus% %i18n:@add-emoji.title%</div> + <section class="fit-top"> + <ui-input v-model="name"> + <span>%i18n:@add-emoji.name%</span> + <span slot="text">%i18n:@add-emoji.name-desc%</span> + </ui-input> + <ui-input v-model="aliases"> + <span>%i18n:@add-emoji.aliases%</span> + <span slot="text">%i18n:@add-emoji.aliases-desc%</span> + </ui-input> + <ui-input v-model="url"> + <span>%i18n:@add-emoji.url%</span> + </ui-input> + <ui-button @click="add">%i18n:@add-emoji.add%</ui-button> + </section> + </ui-card> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; + +export default Vue.extend({ + data() { + return { + name: '', + url: '', + aliases: '', + }; + }, + methods: { + add() { + (this as any).api('admin/add-emoji', { + name: this.name, + url: this.url, + aliases: this.aliases.split(' ') + }).then(() => { + (this as any).os.apis.dialog({ text: `Added` }); + }).catch(e => { + (this as any).os.apis.dialog({ text: `Failed ${e}` }); + }); + } + } +}); +</script> diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue new file mode 100644 index 0000000000..be90cea1db --- /dev/null +++ b/src/client/app/admin/views/hashtags.vue @@ -0,0 +1,46 @@ +<template> +<div> + <ui-card> + <div slot="title">%i18n:@hided-tags%</div> + <section> + <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea> + <ui-button @click="save">%i18n:@save%</ui-button> + </section> + </ui-card> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; + +export default Vue.extend({ + data() { + return { + hidedTags: '', + }; + }, + created() { + (this as any).os.getMeta().then(meta => { + this.hidedTags = meta.hidedTags.join('\n'); + }); + }, + methods: { + save() { + (this as any).api('admin/update-meta', { + hidedTags: this.hidedTags.split('\n') + }).then(() => { + (this as any).os.apis.dialog({ text: `Saved` }); + }).catch(e => { + (this as any).os.apis.dialog({ text: `Failed ${e}` }); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.jdnqwkzlnxcfftthoybjxrebyolvoucw + width 100% + min-height 300px + +</style> diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue new file mode 100644 index 0000000000..c674bee028 --- /dev/null +++ b/src/client/app/admin/views/index.vue @@ -0,0 +1,101 @@ +<template> +<div class="mk-admin"> + <nav> + <ul> + <li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:home .fw%%i18n:@dashboard%</li> + <li @click="nav('instance')" :class="{ active: page == 'instance' }">%fa:cog .fw%%i18n:@instance%</li> + <li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li> + <li @click="nav('emoji')" :class="{ active: page == 'emoji' }">%fa:grin R .fw%%i18n:@emoji%</li> + <li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li> + <li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</li> + + <!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:common.drive%</li> --> + <!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> --> + </ul> + </nav> + <main> + <div v-show="page == 'dashboard'"><x-dashboard/></div> + <div v-show="page == 'instance'"><x-instance/></div> + <div v-if="page == 'users'"><x-users/></div> + <div v-show="page == 'emoji'"><x-emoji/></div> + <div v-show="page == 'announcements'"><x-announcements/></div> + <div v-show="page == 'hashtags'"><x-hashtags/></div> + <div v-if="page == 'drive'"></div> + <div v-if="page == 'update'"></div> + </main> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import XDashboard from "./dashboard.vue"; +import XInstance from "./instance.vue"; +import XEmoji from "./emoji.vue"; +import XAnnouncements from "./announcements.vue"; +import XHashtags from "./hashtags.vue"; +import XUsers from "./users.vue"; + +export default Vue.extend({ + components: { + XDashboard, + XInstance, + XEmoji, + XAnnouncements, + XHashtags, + XUsers + }, + data() { + return { + page: 'dashboard' + }; + }, + methods: { + nav(page: string) { + this.page = page; + } + } +}); +</script> + +<style lang="stylus"> +.mk-admin + display flex + height 100% + + > nav + position fixed + z-index 10000 + top 0 + left 0 + width 250px + height 100vh + padding 16px 0 0 0 + overflow auto + background #333 + color #fff + + > ul + margin 0 + padding 0 + list-style none + + > li + display block + padding 10px 16px + margin 0 + cursor pointer + user-select none + transition margin-left 0.2s ease + + > [data-fa] + margin-right 4px + + &.active + margin-left 8px + color var(--primary) !important + + > main + width 100% + padding 32px 32px 32px calc(32px + 250px) + +</style> diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue new file mode 100644 index 0000000000..542a0d54ad --- /dev/null +++ b/src/client/app/admin/views/instance.vue @@ -0,0 +1,62 @@ +<template> +<div> + <ui-card> + <div slot="title">%i18n:@banner-url%</div> + <section class="fit-top"> + <ui-input v-model="bannerUrl"/> + <ui-button @click="updateMeta">%i18n:@save%</ui-button> + </section> + </ui-card> + + <ui-card> + <div slot="title">%i18n:@disable-registration%</div> + <section> + <input type="checkbox" v-model="disableRegistration" @change="updateMeta"> + <button class="ui" @click="invite">%i18n:@invite%</button> + <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> + </section> + </ui-card> + + <ui-card> + <div slot="title">%i18n:@disable-local-timeline%</div> + <section> + <input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta"> + </section> + </ui-card> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; + +export default Vue.extend({ + data() { + return { + disableRegistration: false, + disableLocalTimeline: false, + bannerUrl: null, + inviteCode: null, + }; + }, + methods: { + invite() { + (this as any).api('admin/invite').then(x => { + this.inviteCode = x.code; + }).catch(e => { + (this as any).os.apis.dialog({ text: `Failed ${e}` }); + }); + }, + updateMeta() { + (this as any).api('admin/update-meta', { + disableRegistration: this.disableRegistration, + disableLocalTimeline: this.disableLocalTimeline, + bannerUrl: this.bannerUrl + }).then(() => { + (this as any).os.apis.dialog({ text: `Saved` }); + }).catch(e => { + (this as any).os.apis.dialog({ text: `Failed ${e}` }); + }); + } + } +}); +</script> diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue new file mode 100644 index 0000000000..3c59943688 --- /dev/null +++ b/src/client/app/admin/views/users.vue @@ -0,0 +1,129 @@ +<template> +<div> + <ui-card> + <div slot="title">%i18n:@verify-user%</div> + <section class="fit-top"> + <ui-input v-model="verifyUsername" type="text"> + <span slot="prefix">@</span> + </ui-input> + <ui-button @click="verifyUser" :disabled="verifying">%i18n:@verify%</ui-button> + </section> + </ui-card> + + <ui-card> + <div slot="title">%i18n:@unverify-user%</div> + <section class="fit-top"> + <ui-input v-model="unverifyUsername" type="text"> + <span slot="prefix">@</span> + </ui-input> + <ui-button @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</ui-button> + </section> + </ui-card> + + <ui-card> + <div slot="title">%i18n:@suspend-user%</div> + <section class="fit-top"> + <ui-input v-model="suspendUsername" type="text"> + <span slot="prefix">@</span> + </ui-input> + <ui-button @click="suspendUser" :disabled="suspending">%i18n:@suspend%</ui-button> + </section> + </ui-card> + + <ui-card> + <div slot="title">%i18n:@unsuspend-user%</div> + <section class="fit-top"> + <ui-input v-model="unsuspendUsername" type="text"> + <span slot="prefix">@</span> + </ui-input> + <ui-button @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</ui-button> + </section> + </ui-card> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import parseAcct from "../../../../misc/acct/parse"; + +export default Vue.extend({ + data() { + return { + verifyUsername: null, + verifying: false, + unverifyUsername: null, + unverifying: false, + suspendUsername: null, + suspending: false, + unsuspendUsername: null, + unsuspending: false + }; + }, + + methods: { + async verifyUser() { + this.verifying = true; + + const process = async () => { + const user = await (this as any).os.api('users/show', parseAcct(this.verifyUsername)); + await (this as any).os.api('admin/verify-user', { userId: user.id }); + (this as any).os.apis.dialog({ text: '%i18n:@verified%' }); + }; + + await process().catch(e => { + (this as any).os.apis.dialog({ text: `Failed: ${e}` }); + }); + + this.verifying = false; + }, + + async unverifyUser() { + this.unverifying = true; + + const process = async () => { + const user = await (this as any).os.api('users/show', parseAcct(this.unverifyUsername)); + await (this as any).os.api('admin/unverify-user', { userId: user.id }); + (this as any).os.apis.dialog({ text: '%i18n:@unverified%' }); + }; + + await process().catch(e => { + (this as any).os.apis.dialog({ text: `Failed: ${e}` }); + }); + + this.unverifying = false; + }, + + async suspendUser() { + this.suspending = true; + + const process = async () => { + const user = await (this as any).os.api('users/show', parseAcct(this.suspendUsername)); + await (this as any).os.api('admin/suspend-user', { userId: user.id }); + (this as any).os.apis.dialog({ text: '%i18n:@suspended%' }); + }; + + await process().catch(e => { + (this as any).os.apis.dialog({ text: `Failed: ${e}` }); + }); + + this.suspending = false; + }, + + async unsuspendUser() { + this.unsuspending = true; + + const process = async () => { + const user = await (this as any).os.api('users/show', parseAcct(this.unsuspendUsername)); + await (this as any).os.api('admin/unsuspend-user', { userId: user.id }); + (this as any).os.apis.dialog({ text: '%i18n:@unsuspended%' }); + }; + + await process().catch(e => { + (this as any).os.apis.dialog({ text: `Failed: ${e}` }); + }); + + this.unsuspending = false; + } + } +}); +</script> |