summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2018-08-18 03:52:24 +0900
committersyuilo <syuilotan@yahoo.co.jp>2018-08-18 03:52:24 +0900
commitbc34ac82cf4effe2baba8471315ea4a78dae416a (patch)
treead5785ad45b344b88078b239eaef3ee725ded4ec /src
parentMerge pull request #2297 from syuilo/update-readme (diff)
downloadsharkey-bc34ac82cf4effe2baba8471315ea4a78dae416a.tar.gz
sharkey-bc34ac82cf4effe2baba8471315ea4a78dae416a.tar.bz2
sharkey-bc34ac82cf4effe2baba8471315ea4a78dae416a.zip
Show some charts in control panel
Diffstat (limited to 'src')
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.dashboard.vue39
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue81
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.notes-chart.vue33
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.suspend-user.vue12
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue53
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.users-chart.vue33
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.vue24
-rw-r--r--src/server/api/endpoints/aggregation/notes.ts110
-rw-r--r--src/server/api/endpoints/aggregation/posts.ts84
-rw-r--r--src/server/api/endpoints/aggregation/users.ts95
10 files changed, 413 insertions, 151 deletions
diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
index d0f11e73b6..182d974601 100644
--- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
@@ -1,11 +1,11 @@
<template>
-<div>
- <h1>%i18n:@dashboard%</h1>
- <div v-if="stats">
- <p><b>%i18n:@all-users%</b>: <span>{{ stats.usersCount | number }}</span></p>
- <p><b>%i18n:@original-users%</b>: <span>{{ stats.originalUsersCount | number }}</span></p>
- <p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p>
- <p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p>
+<div class="obdskegsannmntldydackcpzezagxqfy">
+ <header>%i18n:@dashboard%</header>
+ <div v-if="stats" class="stats">
+ <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
+ <div><b>%fa:user% {{ stats.usersCount | number }}</b><span>%i18n:@all-users%</span></div>
+ <div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
+ <div><b>%fa:pen% {{ stats.notesCount | number }}</b><span>%i18n:@all-notes%</span></div>
</div>
<div>
<button class="ui" @click="invite">%i18n:@invite%</button>
@@ -40,10 +40,23 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
-h1
- margin 0 0 1em 0
- padding 0 0 8px 0
- font-size 1em
- color #555
- border-bottom solid 1px #eee
+@import '~const.styl'
+
+.obdskegsannmntldydackcpzezagxqfy
+ > .stats
+ display flex
+ justify-content center
+ margin-bottom 16px
+
+ > div
+ flex 1
+ text-align center
+
+ > b
+ display block
+ color $theme-color
+
+ > span
+ font-size 70%
+
</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue
new file mode 100644
index 0000000000..52bd8e7c77
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue
@@ -0,0 +1,81 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+ <polyline
+ :points="pointsNote"
+ fill="none"
+ stroke-width="1"
+ stroke="#41ddde"/>
+ <polyline
+ :points="pointsReply"
+ fill="none"
+ stroke-width="1"
+ stroke="#f7796c"/>
+ <polyline
+ :points="pointsRenote"
+ fill="none"
+ stroke-width="1"
+ stroke="#a1de41"/>
+ <polyline
+ :points="pointsTotal"
+ fill="none"
+ stroke-width="1"
+ stroke="#555"
+ stroke-dasharray="2 2"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ data: {
+ required: true
+ },
+ type: {
+ type: String,
+ required: true
+ }
+ },
+ data() {
+ return {
+ chart: this.data,
+ viewBoxX: 365,
+ viewBoxY: 70,
+ pointsNote: null,
+ pointsReply: null,
+ pointsRenote: null,
+ pointsTotal: null
+ };
+ },
+ created() {
+ this.chart.forEach(d => {
+ d.notes = this.type == 'local' ? d.localNotes : d.remoteNotes;
+ d.replies = this.type == 'local' ? d.localReplies : d.remoteReplies;
+ d.renotes = this.type == 'local' ? d.localRenotes : d.remoteRenotes;
+ });
+
+ this.chart.forEach(d => {
+ d.total = d.notes + d.replies + d.renotes;
+ });
+
+ const peak = Math.max.apply(null, this.chart.map(d => d.total));
+
+ if (peak != 0) {
+ const data = this.chart.slice().reverse();
+ this.pointsNote = data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsReply = data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+svg
+ display block
+ padding 10px
+ width 100%
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue
new file mode 100644
index 0000000000..1f36d6b26a
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue
@@ -0,0 +1,33 @@
+<template>
+<div>
+ <header>%i18n:@title%</header>
+ <x-chart v-if="data" :data="data" type="local"/>
+ <x-chart v-if="data" :data="data" type="remote"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+import XChart from "./admin.notes-chart.chart.vue";
+
+export default Vue.extend({
+ components: {
+ XChart
+ },
+ data() {
+ return {
+ data: null
+ };
+ },
+ created() {
+ (this as any).api('aggregation/notes').then(res => {
+ this.data = res;
+ });
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue
index 6eb82f0a51..6d21bdb564 100644
--- a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue
@@ -37,15 +37,3 @@ export default Vue.extend({
}
});
</script>
-
-<style lang="stylus" scoped>
-@import '~const.styl'
-
-header
- margin 10px 0
-
-
-button
- margin 16px 0
-
-</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue
new file mode 100644
index 0000000000..10eab85279
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue
@@ -0,0 +1,53 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
+ <polyline
+ :points="points"
+ fill="none"
+ stroke-width="1"
+ stroke="#555"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ data: {
+ required: true
+ },
+ type: {
+ type: String,
+ required: true
+ }
+ },
+ data() {
+ return {
+ chart: this.data,
+ viewBoxX: 365,
+ viewBoxY: 70,
+ points: null
+ };
+ },
+ created() {
+ this.chart.forEach(d => {
+ d.count = this.type == 'local' ? d.local : d.remote;
+ });
+
+ const peak = Math.max.apply(null, this.chart.map(d => d.count));
+
+ if (peak != 0) {
+ const data = this.chart.slice().reverse();
+ this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' ');
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+svg
+ display block
+ padding 10px
+ width 100%
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue
new file mode 100644
index 0000000000..15c01fc59e
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue
@@ -0,0 +1,33 @@
+<template>
+<div>
+ <header>%i18n:@title%</header>
+ <x-chart v-if="data" :data="data" type="local"/>
+ <x-chart v-if="data" :data="data" type="remote"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+import XChart from "./admin.users-chart.chart.vue";
+
+export default Vue.extend({
+ components: {
+ XChart
+ },
+ data() {
+ return {
+ data: null
+ };
+ },
+ created() {
+ (this as any).api('aggregation/users').then(res => {
+ this.data = res;
+ });
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue
index 2a524bb269..f719ccfda7 100644
--- a/src/client/app/desktop/views/pages/admin/admin.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.vue
@@ -11,6 +11,8 @@
<main>
<div v-if="page == 'dashboard'">
<x-dashboard/>
+ <x-users-chart/>
+ <x-notes-chart/>
</div>
<div v-if="page == 'users'">
<x-suspend-user/>
@@ -29,13 +31,17 @@ import XDashboard from "./admin.dashboard.vue";
import XSuspendUser from "./admin.suspend-user.vue";
import XUnsuspendUser from "./admin.unsuspend-user.vue";
import XVerifyUser from "./admin.verify-user.vue";
+import XUsersChart from "./admin.users-chart.vue";
+import XNotesChart from "./admin.notes-chart.vue";
export default Vue.extend({
components: {
XDashboard,
XSuspendUser,
XUnsuspendUser,
- XVerifyUser
+ XVerifyUser,
+ XUsersChart,
+ XNotesChart
},
data() {
return {
@@ -50,7 +56,7 @@ export default Vue.extend({
});
</script>
-<style lang="stylus" scoped>
+<style lang="stylus">
@import '~const.styl'
.mk-admin
@@ -101,13 +107,11 @@ export default Vue.extend({
background #fff
box-shadow 0 2px 8px rgba(#000, 0.1)
-header
- margin 10px 0
-
-
-button
- margin 16px 0
- position absolute
- right 0
+ > header
+ margin 0 0 1em 0
+ padding 0 0 8px 0
+ font-size 1em
+ color #555
+ border-bottom solid 1px #eee
</style>
diff --git a/src/server/api/endpoints/aggregation/notes.ts b/src/server/api/endpoints/aggregation/notes.ts
new file mode 100644
index 0000000000..b745c86631
--- /dev/null
+++ b/src/server/api/endpoints/aggregation/notes.ts
@@ -0,0 +1,110 @@
+import $ from 'cafy';
+import Note from '../../../../models/note';
+
+/**
+ * Aggregate notes
+ */
+export default (params: any) => new Promise(async (res, rej) => {
+ // Get 'limit' parameter
+ const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit);
+ if (limitErr) return rej('invalid limit param');
+
+ const query = [{
+ $project: {
+ renoteId: '$renoteId',
+ replyId: '$replyId',
+ user: '$_user',
+ createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
+ }
+ }, {
+ $project: {
+ date: {
+ year: { $year: '$createdAt' },
+ month: { $month: '$createdAt' },
+ day: { $dayOfMonth: '$createdAt' }
+ },
+ type: {
+ $cond: {
+ if: { $ne: ['$renoteId', null] },
+ then: 'renote',
+ else: {
+ $cond: {
+ if: { $ne: ['$replyId', null] },
+ then: 'reply',
+ else: 'note'
+ }
+ }
+ }
+ },
+ origin: {
+ $cond: {
+ if: { $eq: ['$user.host', null] },
+ then: 'local',
+ else: 'remote'
+ }
+ }
+ }
+ }, {
+ $group: {
+ _id: {
+ date: '$date',
+ type: '$type',
+ origin: '$origin'
+ },
+ count: { $sum: 1 }
+ }
+ }, {
+ $group: {
+ _id: '$_id.date',
+ data: {
+ $addToSet: {
+ type: '$_id.type',
+ origin: '$_id.origin',
+ count: '$count'
+ }
+ }
+ }
+ }] as any;
+
+ const datas = await Note.aggregate(query);
+
+ datas.forEach((data: any) => {
+ data.date = data._id;
+ delete data._id;
+
+ data.localNotes = (data.data.filter((x: any) => x.type == 'note' && x.origin == 'local')[0] || { count: 0 }).count;
+ data.localRenotes = (data.data.filter((x: any) => x.type == 'renote' && x.origin == 'local')[0] || { count: 0 }).count;
+ data.localReplies = (data.data.filter((x: any) => x.type == 'reply' && x.origin == 'local')[0] || { count: 0 }).count;
+ data.remoteNotes = (data.data.filter((x: any) => x.type == 'note' && x.origin == 'remote')[0] || { count: 0 }).count;
+ data.remoteRenotes = (data.data.filter((x: any) => x.type == 'renote' && x.origin == 'remote')[0] || { count: 0 }).count;
+ data.remoteReplies = (data.data.filter((x: any) => x.type == 'reply' && x.origin == 'remote')[0] || { count: 0 }).count;
+
+ delete data.data;
+ });
+
+ const graph = [];
+
+ for (let i = 0; i < limit; i++) {
+ const day = new Date(new Date().setDate(new Date().getDate() - i));
+
+ const data = datas.filter((d: any) =>
+ d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
+ )[0];
+
+ if (data) {
+ graph.push(data);
+ } else {
+ graph.push({
+ date: { year: day.getFullYear(), month: day.getMonth() + 1, day: day.getDate() },
+ localNotes: 0,
+ localRenotes: 0,
+ localReplies: 0,
+ remoteNotes: 0,
+ remoteRenotes: 0,
+ remoteReplies: 0
+ });
+ }
+ }
+
+ res(graph);
+});
diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts
deleted file mode 100644
index 629bb19108..0000000000
--- a/src/server/api/endpoints/aggregation/posts.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import $ from 'cafy';
-import Note from '../../../../models/note';
-
-/**
- * Aggregate notes
- */
-export default (params: any) => new Promise(async (res, rej) => {
- // Get 'limit' parameter
- const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit);
- if (limitErr) return rej('invalid limit param');
-
- const datas = await Note
- .aggregate([
- { $project: {
- renoteId: '$renoteId',
- replyId: '$replyId',
- createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
- }},
- { $project: {
- date: {
- year: { $year: '$createdAt' },
- month: { $month: '$createdAt' },
- day: { $dayOfMonth: '$createdAt' }
- },
- type: {
- $cond: {
- if: { $ne: ['$renoteId', null] },
- then: 'renote',
- else: {
- $cond: {
- if: { $ne: ['$replyId', null] },
- then: 'reply',
- else: 'note'
- }
- }
- }
- }}
- },
- { $group: { _id: {
- date: '$date',
- type: '$type'
- }, count: { $sum: 1 } } },
- { $group: {
- _id: '$_id.date',
- data: { $addToSet: {
- type: '$_id.type',
- count: '$count'
- }}
- } }
- ]);
-
- datas.forEach((data: any) => {
- data.date = data._id;
- delete data._id;
-
- data.notes = (data.data.filter((x: any) => x.type == 'note')[0] || { count: 0 }).count;
- data.renotes = (data.data.filter((x: any) => x.type == 'renote')[0] || { count: 0 }).count;
- data.replies = (data.data.filter((x: any) => x.type == 'reply')[0] || { count: 0 }).count;
-
- delete data.data;
- });
-
- const graph = [];
-
- for (let i = 0; i < limit; i++) {
- const day = new Date(new Date().setDate(new Date().getDate() - i));
-
- const data = datas.filter((d: any) =>
- d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
- )[0];
-
- if (data) {
- graph.push(data);
- } else {
- graph.push({
- notes: 0,
- renotes: 0,
- replies: 0
- });
- }
- }
-
- res(graph);
-});
diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts
index f1e41cf170..2e397545de 100644
--- a/src/server/api/endpoints/aggregation/users.ts
+++ b/src/server/api/endpoints/aggregation/users.ts
@@ -9,46 +9,77 @@ export default (params: any) => new Promise(async (res, rej) => {
const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit);
if (limitErr) return rej('invalid limit param');
- const users = await User
- .find({}, {
- sort: {
- _id: -1
+ const query = [{
+ $project: {
+ host: '$host',
+ createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
+ }
+ }, {
+ $project: {
+ date: {
+ year: { $year: '$createdAt' },
+ month: { $month: '$createdAt' },
+ day: { $dayOfMonth: '$createdAt' }
},
- fields: {
- _id: false,
- createdAt: true,
- deletedAt: true
+ origin: {
+ $cond: {
+ if: { $eq: ['$host', null] },
+ then: 'local',
+ else: 'remote'
+ }
}
- });
+ }
+ }, {
+ $group: {
+ _id: {
+ date: '$date',
+ origin: '$origin'
+ },
+ count: { $sum: 1 }
+ }
+ }, {
+ $group: {
+ _id: '$_id.date',
+ data: {
+ $addToSet: {
+ type: '$_id.type',
+ origin: '$_id.origin',
+ count: '$count'
+ }
+ }
+ }
+ }] as any;
- const graph = [];
+ const datas = await User.aggregate(query);
- for (let i = 0; i < limit; i++) {
- let dayStart = new Date(new Date().setDate(new Date().getDate() - i));
- dayStart = new Date(dayStart.setMilliseconds(0));
- dayStart = new Date(dayStart.setSeconds(0));
- dayStart = new Date(dayStart.setMinutes(0));
- dayStart = new Date(dayStart.setHours(0));
+ datas.forEach((data: any) => {
+ data.date = data._id;
+ delete data._id;
- let dayEnd = new Date(new Date().setDate(new Date().getDate() - i));
- dayEnd = new Date(dayEnd.setMilliseconds(999));
- dayEnd = new Date(dayEnd.setSeconds(59));
- dayEnd = new Date(dayEnd.setMinutes(59));
- dayEnd = new Date(dayEnd.setHours(23));
- // day = day.getTime();
+ data.local = (data.data.filter((x: any) => x.origin == 'local')[0] || { count: 0 }).count;
+ data.remote = (data.data.filter((x: any) => x.origin == 'remote')[0] || { count: 0 }).count;
- const total = users.filter(u =>
- u.createdAt < dayEnd && (u.deletedAt == null || u.deletedAt > dayEnd)
- ).length;
+ delete data.data;
+ });
+
+ const graph = [];
+
+ for (let i = 0; i < limit; i++) {
+ const day = new Date(new Date().setDate(new Date().getDate() - i));
- const created = users.filter(u =>
- u.createdAt < dayEnd && u.createdAt > dayStart
- ).length;
+ const data = datas.filter((d: any) =>
+ d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
+ )[0];
- graph.push({
- total: total,
- created: created
- });
+ if (data) {
+ graph.push(data);
+ } else {
+ graph.push({
+ date: { year: day.getFullYear(), month: day.getMonth() + 1, day: day.getDate() },
+ local: 0,
+ remote: 0
+ });
+ }
}
res(graph);