summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2018-08-19 00:46:17 +0900
committerGitHub <noreply@github.com>2018-08-19 00:46:17 +0900
commit2f45ac37127b6651c5e6b39512d40f997088f2bd (patch)
treeb245c7f056b475cd3026f4a62f2825bb8cc842d5 /src
parentmissing semicolon (diff)
parentAdd drive chart (diff)
downloadmisskey-2f45ac37127b6651c5e6b39512d40f997088f2bd.tar.gz
misskey-2f45ac37127b6651c5e6b39512d40f997088f2bd.tar.bz2
misskey-2f45ac37127b6651c5e6b39512d40f997088f2bd.zip
Merge pull request #2329 from syuilo/improve-chart
Improve chart
Diffstat (limited to 'src')
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue51
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.drive-chart.vue34
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue29
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.notes-chart.vue17
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue14
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.users-chart.vue17
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.vue17
-rw-r--r--src/client/app/stats/style.styl10
-rw-r--r--src/client/app/stats/tags/index.tag209
-rw-r--r--src/client/app/stats/tags/index.ts1
-rw-r--r--src/client/app/status/style.styl10
-rw-r--r--src/client/app/status/tags/index.tag201
-rw-r--r--src/client/app/status/tags/index.ts1
-rw-r--r--src/models/drive-file.ts5
-rw-r--r--src/models/stats.ts153
-rw-r--r--src/server/api/endpoints/admin/chart.ts101
-rw-r--r--src/server/api/endpoints/aggregation/notes.ts116
-rw-r--r--src/server/api/endpoints/aggregation/users.ts92
-rw-r--r--src/services/drive/add-file.ts4
-rw-r--r--src/services/drive/delete-file.ts4
-rw-r--r--src/services/note/create.ts4
-rw-r--r--src/services/note/delete.ts4
-rw-r--r--src/services/update-chart.ts223
23 files changed, 625 insertions, 692 deletions
diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue
new file mode 100644
index 0000000000..3c537d8d6d
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue
@@ -0,0 +1,51 @@
+<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: {
+ chart: {
+ required: true
+ },
+ type: {
+ type: String,
+ required: true
+ }
+ },
+ data() {
+ return {
+ viewBoxX: 365,
+ viewBoxY: 70,
+ points: null
+ };
+ },
+ created() {
+ const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize));
+
+ if (peak != 0) {
+ const data = this.chart.slice().reverse().map(x => ({
+ size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize
+ }));
+
+ this.points = data.map((d, i) => `${i},${(1 - (d.size / 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.drive-chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue
new file mode 100644
index 0000000000..4f94fd2372
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue
@@ -0,0 +1,34 @@
+<template>
+<div class="card">
+ <header>%i18n:@title%</header>
+ <div class="card">
+ <header>%i18n:@local%</header>
+ <x-chart v-if="chart" :chart="chart" type="local"/>
+ </div>
+ <div class="card">
+ <header>%i18n:@remote%</header>
+ <x-chart v-if="chart" :chart="chart" type="remote"/>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+import XChart from "./admin.drive-chart.chart.vue";
+
+export default Vue.extend({
+ components: {
+ XChart
+ },
+ props: {
+ chart: {
+ required: true
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+</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
index 52bd8e7c77..83c61c1313 100644
--- 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
@@ -29,7 +29,7 @@ import Vue from 'vue';
export default Vue.extend({
props: {
- data: {
+ chart: {
required: true
},
type: {
@@ -39,7 +39,6 @@ export default Vue.extend({
},
data() {
return {
- chart: this.data,
viewBoxX: 365,
viewBoxY: 70,
pointsNote: null,
@@ -49,23 +48,19 @@ export default Vue.extend({
};
},
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));
+ const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff));
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(' ');
+ const data = this.chart.slice().reverse().map(x => ({
+ normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal,
+ reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply,
+ renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote,
+ total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff
+ }));
+
+ this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' ');
+ this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' ');
this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
}
}
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
index 8b33e59ec3..e4d396d9c6 100644
--- a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue
@@ -3,11 +3,11 @@
<header>%i18n:@title%</header>
<div class="card">
<header>%i18n:@local%</header>
- <x-chart v-if="data" :data="data" type="local"/>
+ <x-chart v-if="chart" :chart="chart" type="local"/>
</div>
<div class="card">
<header>%i18n:@remote%</header>
- <x-chart v-if="data" :data="data" type="remote"/>
+ <x-chart v-if="chart" :chart="chart" type="remote"/>
</div>
</div>
</template>
@@ -20,15 +20,10 @@ export default Vue.extend({
components: {
XChart
},
- data() {
- return {
- data: null
- };
- },
- created() {
- (this as any).api('aggregation/notes').then(res => {
- this.data = res;
- });
+ props: {
+ chart: {
+ required: true
+ }
}
});
</script>
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
index 10eab85279..c2ab4a78e3 100644
--- 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
@@ -13,7 +13,7 @@ import Vue from 'vue';
export default Vue.extend({
props: {
- data: {
+ chart: {
required: true
},
type: {
@@ -23,21 +23,19 @@ export default Vue.extend({
},
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));
+ const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff));
if (peak != 0) {
- const data = this.chart.slice().reverse();
+ const data = this.chart.slice().reverse().map(x => ({
+ count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff
+ }));
+
this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' ');
}
}
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
index bbd342e515..e620012702 100644
--- a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue
@@ -3,11 +3,11 @@
<header>%i18n:@title%</header>
<div class="card">
<header>%i18n:@local%</header>
- <x-chart v-if="data" :data="data" type="local"/>
+ <x-chart v-if="chart" :chart="chart" type="local"/>
</div>
<div class="card">
<header>%i18n:@remote%</header>
- <x-chart v-if="data" :data="data" type="remote"/>
+ <x-chart v-if="chart" :chart="chart" type="remote"/>
</div>
</div>
</template>
@@ -20,15 +20,10 @@ export default Vue.extend({
components: {
XChart
},
- data() {
- return {
- data: null
- };
- },
- created() {
- (this as any).api('aggregation/users').then(res => {
- this.data = res;
- });
+ props: {
+ chart: {
+ required: true
+ }
}
});
</script>
diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue
index 7c1dace78c..cbb1890cc3 100644
--- a/src/client/app/desktop/views/pages/admin/admin.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.vue
@@ -11,8 +11,9 @@
<main>
<div v-show="page == 'dashboard'">
<x-dashboard/>
- <x-users-chart/>
- <x-notes-chart/>
+ <x-users-chart :chart="chart"/>
+ <x-notes-chart :chart="chart"/>
+ <x-drive-chart :chart="chart"/>
</div>
<div v-if="page == 'users'">
<x-suspend-user/>
@@ -35,6 +36,7 @@ import XVerifyUser from "./admin.verify-user.vue";
import XUnverifyUser from "./admin.unverify-user.vue";
import XUsersChart from "./admin.users-chart.vue";
import XNotesChart from "./admin.notes-chart.vue";
+import XDriveChart from "./admin.drive-chart.vue";
export default Vue.extend({
components: {
@@ -44,13 +46,20 @@ export default Vue.extend({
XVerifyUser,
XUnverifyUser,
XUsersChart,
- XNotesChart
+ XNotesChart,
+ XDriveChart
},
data() {
return {
- page: 'dashboard'
+ page: 'dashboard',
+ chart: null
};
},
+ created() {
+ (this as any).api('admin/chart').then(chart => {
+ this.chart = chart;
+ });
+ },
methods: {
nav(page: string) {
this.page = page;
diff --git a/src/client/app/stats/style.styl b/src/client/app/stats/style.styl
deleted file mode 100644
index 5ae230ea56..0000000000
--- a/src/client/app/stats/style.styl
+++ /dev/null
@@ -1,10 +0,0 @@
-@import "../app"
-@import "../reset"
-
-html
- color #456267
- background #fff
-
-body
- margin 0
- padding 0
diff --git a/src/client/app/stats/tags/index.tag b/src/client/app/stats/tags/index.tag
deleted file mode 100644
index f8944c0832..0000000000
--- a/src/client/app/stats/tags/index.tag
+++ /dev/null
@@ -1,209 +0,0 @@
-<mk-index>
- <h1>Misskey<i>Statistics</i></h1>
- <main v-if="!initializing">
- <mk-users stats={ stats }/>
- <mk-notes stats={ stats }/>
- </main>
- <footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
- <style lang="stylus" scoped>
- :scope
- display block
- margin 0 auto
- padding 0 16px
- max-width 700px
-
- > h1
- margin 0
- padding 24px 0 0 0
- font-size 24px
- font-weight normal
-
- > i
- font-style normal
- color #f43b16
-
- > main
- > *
- margin 24px 0
- padding-top 24px
- border-top solid 1px #eee
-
- > h2
- margin 0 0 12px 0
- font-size 18px
- font-weight normal
-
- > footer
- margin 24px 0
- text-align center
-
- > a
- color #546567
- </style>
- <script lang="typescript">
- this.mixin('api');
-
- this.initializing = true;
-
- this.on('mount', () => {
- this.$root.$data.os.api('stats').then(stats => {
- this.update({
- initializing: false,
- stats
- });
- });
- });
- </script>
-</mk-index>
-
-<mk-notes>
- <h2>%i18n:stats.notes-count% <b>{ stats.notesCount }</b></h2>
- <mk-notes-chart v-if="!initializing" data={ data }/>
- <style lang="stylus" scoped>
- :scope
- display block
- </style>
- <script lang="typescript">
- this.mixin('api');
-
- this.initializing = true;
- this.stats = this.opts.stats;
-
- this.on('mount', () => {
- this.$root.$data.os.api('aggregation/notes', {
- limit: 365
- }).then(data => {
- this.update({
- initializing: false,
- data
- });
- });
- });
- </script>
-</mk-notes>
-
-<mk-users>
- <h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2>
- <mk-users-chart v-if="!initializing" data={ data }/>
- <style lang="stylus" scoped>
- :scope
- display block
- </style>
- <script lang="typescript">
- this.mixin('api');
-
- this.initializing = true;
- this.stats = this.opts.stats;
-
- this.on('mount', () => {
- this.$root.$data.os.api('aggregation/users', {
- limit: 365
- }).then(data => {
- this.update({
- initializing: false,
- data
- });
- });
- });
- </script>
-</mk-users>
-
-<mk-notes-chart>
- <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
- <title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title>
- <polyline
- riot-points={ pointsNote }
- fill="none"
- stroke-width="1"
- stroke="#41ddde"/>
- <polyline
- riot-points={ pointsReply }
- fill="none"
- stroke-width="1"
- stroke="#f7796c"/>
- <polyline
- riot-points={ pointsRenote }
- fill="none"
- stroke-width="1"
- stroke="#a1de41"/>
- <polyline
- riot-points={ pointsTotal }
- fill="none"
- stroke-width="1"
- stroke="#555"
- stroke-dasharray="2 2"/>
- </svg>
- <style lang="stylus" scoped>
- :scope
- display block
-
- > svg
- display block
- padding 1px
- width 100%
- </style>
- <script lang="typescript">
- this.viewBoxX = 365;
- this.viewBoxY = 80;
-
- this.data = this.opts.data.reverse();
- this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
- const peak = Math.max.apply(null, this.data.map(d => d.total));
-
- this.on('mount', () => {
- this.render();
- });
-
- this.render = () => {
- this.update({
- pointsNote: this.data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '),
- pointsReply: this.data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '),
- pointsRenote: this.data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '),
- pointsTotal: this.data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ')
- });
- };
- </script>
-</mk-notes-chart>
-
-<mk-users-chart>
- <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
- <polyline
- riot-points={ createdPoints }
- fill="none"
- stroke-width="1"
- stroke="#1cde84"/>
- <polyline
- riot-points={ totalPoints }
- fill="none"
- stroke-width="1"
- stroke="#555"/>
- </svg>
- <style lang="stylus" scoped>
- :scope
- display block
-
- > svg
- display block
- padding 1px
- width 100%
- </style>
- <script lang="typescript">
- this.viewBoxX = 365;
- this.viewBoxY = 80;
-
- this.data = this.opts.data.reverse();
- const totalPeak = Math.max.apply(null, this.data.map(d => d.total));
- const createdPeak = Math.max.apply(null, this.data.map(d => d.created));
-
- this.on('mount', () => {
- this.render();
- });
-
- this.render = () => {
- this.update({
- totalPoints: this.data.map((d, i) => `${i},${(1 - (d.total / totalPeak)) * this.viewBoxY}`).join(' '),
- createdPoints: this.data.map((d, i) => `${i},${(1 - (d.created / createdPeak)) * this.viewBoxY}`).join(' ')
- });
- };
- </script>
-</mk-users-chart>
diff --git a/src/client/app/stats/tags/index.ts b/src/client/app/stats/tags/index.ts
deleted file mode 100644
index f41151949f..0000000000
--- a/src/client/app/stats/tags/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-require('./index.tag');
diff --git a/src/client/app/status/style.styl b/src/client/app/status/style.styl
deleted file mode 100644
index 5ae230ea56..0000000000
--- a/src/client/app/status/style.styl
+++ /dev/null
@@ -1,10 +0,0 @@
-@import "../app"
-@import "../reset"
-
-html
- color #456267
- background #fff
-
-body
- margin 0
- padding 0
diff --git a/src/client/app/status/tags/index.tag b/src/client/app/status/tags/index.tag
deleted file mode 100644
index 899467097a..0000000000
--- a/src/client/app/status/tags/index.tag
+++ /dev/null
@@ -1,201 +0,0 @@
-<mk-index>
- <h1>Misskey<i>Status</i></h1>
- <p>%fa:info-circle%%i18n:status.all-systems-maybe-operational%</p>
- <main>
- <mk-cpu-usage connection={ connection }/>
- <mk-mem-usage connection={ connection }/>
- </main>
- <footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
- <style lang="stylus" scoped>
- :scope
- display block
- margin 0 auto
- padding 0 16px
- max-width 700px
-
- > h1
- margin 0
- padding 24px 0 16px 0
- font-size 24px
- font-weight normal
-
- > [data-fa]
- font-style normal
- color #f43b16
-
- > p
- display block
- margin 0
- padding 12px 16px
- background #eaf4ef
- //border solid 1px #99ccb2
- border-radius 4px
-
- > [data-fa]
- margin-right 5px
-
- > main
- > *
- margin 24px 0
-
- > h2
- margin 0 0 12px 0
- font-size 18px
- font-weight normal
-
- > footer
- margin 24px 0
- text-align center
-
- > a
- color #546567
- </style>
- <script lang="typescript">
- import Connection from '../../common/scripts/streaming/server-stream';
-
- this.mixin('api');
-
- this.initializing = true;
- this.connection = new Connection();
-
- this.on('mount', () => {
- this.$root.$data.os.api('meta').then(meta => {
- this.update({
- initializing: false,
- meta
- });
- });
- });
-
- this.on('unmount', () => {
- this.connection.close();
- });
-
- </script>
-</mk-index>
-
-<mk-cpu-usage>
- <h2>CPU <b>{ percentage }%</b></h2>
- <mk-line-chart ref="chart"/>
- <style lang="stylus" scoped>
- :scope
- display block
- </style>
- <script lang="typescript">
- this.connection = this.opts.connection;
-
- this.on('mount', () => {
- this.connection.on('stats', this.onStats);
- });
-
- this.on('unmount', () => {
- this.connection.off('stats', this.onStats);
- });
-
- this.onStats = stats => {
- this.$refs.chart.addData(1 - stats.cpu_usage);
-
- const percentage = (stats.cpu_usage * 100).toFixed(0);
-
- this.update({
- percentage
- });
- };
- </script>
-</mk-cpu-usage>
-
-<mk-mem-usage>
- <h2>MEM <b>{ percentage }%</b></h2>
- <mk-line-chart ref="chart"/>
- <style lang="stylus" scoped>
- :scope
- display block
- </style>
- <script lang="typescript">
- this.connection = this.opts.connection;
-
- this.on('mount', () => {
- this.connection.on('stats', this.onStats);
- });
-
- this.on('unmount', () => {
- this.connection.off('stats', this.onStats);
- });
-
- this.onStats = stats => {
- stats.mem.used = stats.mem.total - stats.mem.free;
- this.$refs.chart.addData(1 - (stats.mem.used / stats.mem.total));
-
- const percentage = (stats.mem.used / stats.mem.total * 100).toFixed(0);
-
- this.update({
- percentage
- });
- };
- </script>
-</mk-mem-usage>
-
-<mk-line-chart>
- <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
- <defs>
- <linearGradient id={ gradientId } x1="0" x2="0" y1="1" y2="0">
- <stop offset="0%" stop-color="rgba(244, 59, 22, 0)"></stop>
- <stop offset="100%" stop-color="#f43b16"></stop>
- </linearGradient>
- <mask id={ maskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }>
- <polygon
- riot-points={ polygonPoints }
- fill="#fff"
- fill-opacity="0.5"/>
- </mask>
- </defs>
- <line x1="0" y1="0" riot-x2={ viewBoxX } y2="0" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
- <line x1="0" y1="25%" riot-x2={ viewBoxX } y2="25%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
- <line x1="0" y1="50%" riot-x2={ viewBoxX } y2="50%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
- <line x1="0" y1="75%" riot-x2={ viewBoxX } y2="75%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
- <line x1="0" y1="100%" riot-x2={ viewBoxX } y2="100%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
- <rect
- x="-1" y="-1"
- riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 }
- style="stroke: none; fill: url(#{ gradientId }); mask: url(#{ maskId })"/>
- <polyline
- riot-points={ polylinePoints }
- fill="none"
- stroke="#f43b16"
- stroke-width="0.5"/>
- </svg>
- <style lang="stylus" scoped>
- :scope
- display block
- padding 16px
- border-radius 8px
- background #1c2531
-
- > svg
- display block
- padding 1px
- width 100%
- </style>
- <script lang="typescript">
- import uuid from 'uuid';
-
- this.viewBoxX = 100;
- this.viewBoxY = 30;
- this.data = [];
- this.gradientId = uuid();
- this.maskId = uuid();
-
- this.addData = data => {
- this.data.push(data);
- if (this.data.length > 100) this.data.shift();
-
- const polylinePoints = this.data.map((d, i) => `${this.viewBoxX - ((this.data.length - 1) - i)},${d * this.viewBoxY}`).join(' ');
- const polygonPoints = `${this.viewBoxX - (this.data.length - 1)},${ this.viewBoxY } ${ polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
-
- this.update({
- polylinePoints,
- polygonPoints
- });
- };
- </script>
-</mk-line-chart>
diff --git a/src/client/app/status/tags/index.ts b/src/client/app/status/tags/index.ts
deleted file mode 100644
index f41151949f..0000000000
--- a/src/client/app/status/tags/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-require('./index.tag');
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index 2b9efc404d..dbbc1f1cd5 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -52,6 +52,11 @@ export type IDriveFile = {
filename: string;
contentType: string;
metadata: IMetadata;
+
+ /**
+ * ファイルサイズ
+ */
+ length: number;
};
export function validateFileName(name: string): boolean {
diff --git a/src/models/stats.ts b/src/models/stats.ts
new file mode 100644
index 0000000000..7bff475c63
--- /dev/null
+++ b/src/models/stats.ts
@@ -0,0 +1,153 @@
+import * as mongo from 'mongodb';
+import db from '../db/mongodb';
+
+const Stats = db.get<IStats>('stats');
+Stats.createIndex({ date: -1 }, { unique: true });
+export default Stats;
+
+export interface IStats {
+ _id: mongo.ObjectID;
+
+ date: Date;
+
+ /**
+ * ユーザーに関する統計
+ */
+ users: {
+ local: {
+ /**
+ * この日時点での、ローカルのユーザーの総計
+ */
+ total: number;
+
+ /**
+ * ローカルのユーザー数の前日比
+ */
+ diff: number;
+ };
+
+ remote: {
+ /**
+ * この日時点での、リモートのユーザーの総計
+ */
+ total: number;
+
+ /**
+ * リモートのユーザー数の前日比
+ */
+ diff: number;
+ };
+ };
+
+ /**
+ * 投稿に関する統計
+ */
+ notes: {
+ local: {
+ /**
+ * この日時点での、ローカルの投稿の総計
+ */
+ total: number;
+
+ /**
+ * ローカルの投稿数の前日比
+ */
+ diff: number;
+
+ diffs: {
+ /**
+ * ローカルの通常の投稿数の前日比
+ */
+ normal: number;
+
+ /**
+ * ローカルのリプライの投稿数の前日比
+ */
+ reply: number;
+
+ /**
+ * ローカルのRenoteの投稿数の前日比
+ */
+ renote: number;
+ };
+ };
+
+ remote: {
+ /**
+ * この日時点での、リモートの投稿の総計
+ */
+ total: number;
+
+ /**
+ * リモートの投稿数の前日比
+ */
+ diff: number;
+
+ diffs: {
+ /**
+ * リモートの通常の投稿数の前日比
+ */
+ normal: number;
+
+ /**
+ * リモートのリプライの投稿数の前日比
+ */
+ reply: number;
+
+ /**
+ * リモートのRenoteの投稿数の前日比
+ */
+ renote: number;
+ };
+ };
+ };
+
+ /**
+ * ドライブ(のファイル)に関する統計
+ */
+ drive: {
+ local: {
+ /**
+ * この日時点での、ローカルのドライブファイル数の総計
+ */
+ totalCount: number;
+
+ /**
+ * この日時点での、ローカルのドライブファイルサイズの総計
+ */
+ totalSize: number;
+
+ /**
+ * ローカルのドライブファイル数の前日比
+ */
+ diffCount: number;
+
+ /**
+ * ローカルのドライブファイルサイズの前日比
+ */
+ diffSize: number;
+ };
+
+ remote: {
+ /**
+ * この日時点での、リモートのドライブファイル数の総計
+ */
+ totalCount: number;
+
+ /**
+ * この日時点での、リモートのドライブファイルサイズの総計
+ */
+ totalSize: number;
+
+ /**
+ * リモートのドライブファイル数の前日比
+ */
+ diffCount: number;
+
+ /**
+ * リモートのドライブファイルサイズの前日比
+ */
+ diffSize: number;
+ };
+ };
+}
diff --git a/src/server/api/endpoints/admin/chart.ts b/src/server/api/endpoints/admin/chart.ts
new file mode 100644
index 0000000000..a0566b11f5
--- /dev/null
+++ b/src/server/api/endpoints/admin/chart.ts
@@ -0,0 +1,101 @@
+import Stats, { IStats } from '../../../../models/stats';
+
+type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
+
+export const meta = {
+ requireCredential: true,
+ requireAdmin: true
+};
+
+export default (params: any) => new Promise(async (res, rej) => {
+ const now = new Date();
+ const y = now.getFullYear();
+ const m = now.getMonth();
+ const d = now.getDate();
+
+ const stats = await Stats.find({
+ date: {
+ $gt: new Date(y - 1, m, d)
+ }
+ }, {
+ sort: {
+ date: -1
+ },
+ fields: {
+ _id: 0
+ }
+ });
+
+ const chart: Array<Omit<IStats, '_id'>> = [];
+
+ for (let i = 364; i >= 0; i--) {
+ const day = new Date(y, m, d - i);
+
+ const stat = stats.find(s => s.date.getTime() == day.getTime());
+
+ if (stat) {
+ chart.unshift(stat);
+ } else { // 隙間埋め
+ const mostRecent = stats.find(s => s.date.getTime() < day.getTime());
+ if (mostRecent) {
+ chart.unshift(Object.assign({}, mostRecent, {
+ date: day
+ }));
+ } else {
+ chart.unshift({
+ date: day,
+ users: {
+ local: {
+ total: 0,
+ diff: 0
+ },
+ remote: {
+ total: 0,
+ diff: 0
+ }
+ },
+ notes: {
+ local: {
+ total: 0,
+ diff: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ },
+ remote: {
+ total: 0,
+ diff: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ }
+ },
+ drive: {
+ local: {
+ totalCount: 0,
+ totalSize: 0,
+ diffCount: 0,
+ diffSize: 0
+ },
+ remote: {
+ totalCount: 0,
+ totalSize: 0,
+ diffCount: 0,
+ diffSize: 0
+ }
+ }
+ });
+ }
+ }
+ }
+
+ chart.forEach(x => {
+ delete x.date;
+ });
+
+ res(chart);
+});
diff --git a/src/server/api/endpoints/aggregation/notes.ts b/src/server/api/endpoints/aggregation/notes.ts
deleted file mode 100644
index 77ed07ef4b..0000000000
--- a/src/server/api/endpoints/aggregation/notes.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import Note from '../../../../models/note';
-
-export const meta = {
- requireCredential: true,
- requireAdmin: true
-};
-
-/**
- * Aggregate notes
- */
-export default (params: any) => new Promise(async (res, rej) => {
- const query = [{
- $match: {
- createdAt: {
- $gt: new Date(new Date().setFullYear(new Date().getFullYear() - 1))
- }
- }
- }, {
- $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 < 365; 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/users.ts b/src/server/api/endpoints/aggregation/users.ts
deleted file mode 100644
index d016484238..0000000000
--- a/src/server/api/endpoints/aggregation/users.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import User from '../../../../models/user';
-
-export const meta = {
- requireCredential: true,
- requireAdmin: true
-};
-
-/**
- * Aggregate users
- */
-export default (params: any) => new Promise(async (res, rej) => {
- const query = [{
- $match: {
- createdAt: {
- $gt: new Date(new Date().setFullYear(new Date().getFullYear() - 1))
- }
- }
- }, {
- $project: {
- host: '$host',
- createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
- }
- }, {
- $project: {
- date: {
- year: { $year: '$createdAt' },
- month: { $month: '$createdAt' },
- day: { $dayOfMonth: '$createdAt' }
- },
- 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 datas = await User.aggregate(query);
-
- datas.forEach((data: any) => {
- data.date = data._id;
- delete data._id;
-
- 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;
-
- delete data.data;
- });
-
- const graph = [];
-
- for (let i = 0; i < 365; 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() },
- local: 0,
- remote: 0
- });
- }
- }
-
- res(graph);
-});
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index da0d3fd82f..b090d56cee 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -17,6 +17,7 @@ import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import delFile from './delete-file';
import config from '../../config';
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
+import { updateDriveStats } from '../update-chart';
const log = debug('misskey:drive:add-file');
@@ -377,7 +378,8 @@ export default async function(
publishDriveStream(user._id, 'file_created', packedFile);
});
- // TODO: サムネイル生成
+ // 統計を更新
+ updateDriveStats(driveFile, true);
return driveFile;
}
diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts
index 445d231d66..73532a2953 100644
--- a/src/services/drive/delete-file.ts
+++ b/src/services/drive/delete-file.ts
@@ -2,6 +2,7 @@ import * as Minio from 'minio';
import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
import config from '../../config';
+import { updateDriveStats } from '../update-chart';
export default async function(file: IDriveFile, isExpired = false) {
if (file.metadata.storage == 'minio') {
@@ -45,4 +46,7 @@ export default async function(file: IDriveFile, isExpired = false) {
await DriveFileThumbnail.remove({ _id: thumbnail._id });
}
//#endregion
+
+ // 統計を更新
+ updateDriveStats(file, false);
}
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 521750dc84..d8f0f57b63 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -23,6 +23,7 @@ import registerHashtag from '../register-hashtag';
import isQuote from '../../misc/is-quote';
import { TextElementMention } from '../../mfm/parse/elements/mention';
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
+import { updateNoteStats } from '../update-chart';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -142,6 +143,9 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
return;
}
+ // 統計を更新
+ updateNoteStats(note, true);
+
// ハッシュタグ登録
tags.map(tag => registerHashtag(user, tag));
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index 7f245958b0..d444b13a8b 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -6,6 +6,7 @@ import pack from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import Following from '../../models/following';
import renderNote from '../../remote/activitypub/renderer/note';
+import { updateNoteStats } from '../update-chart';
/**
* 投稿を削除します。
@@ -43,4 +44,7 @@ export default async function(user: IUser, note: INote) {
});
}
//#endregion
+
+ // 統計を更新
+ updateNoteStats(note, false);
}
diff --git a/src/services/update-chart.ts b/src/services/update-chart.ts
new file mode 100644
index 0000000000..7998baca9d
--- /dev/null
+++ b/src/services/update-chart.ts
@@ -0,0 +1,223 @@
+import { INote } from '../models/note';
+import Stats, { IStats } from '../models/stats';
+import { isLocalUser, IUser } from '../models/user';
+import { IDriveFile } from '../models/drive-file';
+
+type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
+
+async function getTodayStats(): Promise<IStats> {
+ const now = new Date();
+ const y = now.getFullYear();
+ const m = now.getMonth();
+ const d = now.getDate();
+ const today = new Date(y, m, d);
+
+ // 今日の統計
+ const todayStats = await Stats.findOne({
+ date: today
+ });
+
+ // 日付が変わってから、初めてのチャート更新なら
+ if (todayStats == null) {
+ // 最も最近の統計を持ってくる
+ // * 昨日何もチャートを更新するような出来事がなかった場合は、
+ // 統計がそもそも作られずドキュメントが存在しないということがあり得るため、
+ // 「昨日の」と決め打ちせずに「もっとも最近の」とします
+ const mostRecentStats = await Stats.findOne({}, {
+ sort: {
+ date: -1
+ }
+ });
+
+ // 統計が存在しなかったら
+ // * Misskeyインスタンスを建てて初めてのチャート更新時など
+ if (mostRecentStats == null) {
+ // 空の統計を作成
+ const chart: Omit<IStats, '_id'> = {
+ date: today,
+ users: {
+ local: {
+ total: 0,
+ diff: 0
+ },
+ remote: {
+ total: 0,
+ diff: 0
+ }
+ },
+ notes: {
+ local: {
+ total: 0,
+ diff: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ },
+ remote: {
+ total: 0,
+ diff: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ }
+ },
+ drive: {
+ local: {
+ totalCount: 0,
+ totalSize: 0,
+ diffCount: 0,
+ diffSize: 0
+ },
+ remote: {
+ totalCount: 0,
+ totalSize: 0,
+ diffCount: 0,
+ diffSize: 0
+ }
+ }
+ };
+
+ const stats = await Stats.insert(chart);
+
+ return stats;
+ } else {
+ // 今日の統計を初期挿入
+ const chart: Omit<IStats, '_id'> = {
+ date: today,
+ users: {
+ local: {
+ total: mostRecentStats.users.local.total,
+ diff: 0
+ },
+ remote: {
+ total: mostRecentStats.users.remote.total,
+ diff: 0
+ }
+ },
+ notes: {
+ local: {
+ total: mostRecentStats.notes.local.total,
+ diff: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ },
+ remote: {
+ total: mostRecentStats.notes.remote.total,
+ diff: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ }
+ },
+ drive: {
+ local: {
+ totalCount: mostRecentStats.drive.local.totalCount,
+ totalSize: mostRecentStats.drive.local.totalSize,
+ diffCount: 0,
+ diffSize: 0
+ },
+ remote: {
+ totalCount: mostRecentStats.drive.remote.totalCount,
+ totalSize: mostRecentStats.drive.remote.totalSize,
+ diffCount: 0,
+ diffSize: 0
+ }
+ }
+ };
+
+ const stats = await Stats.insert(chart);
+
+ return stats;
+ }
+ } else {
+ return todayStats;
+ }
+}
+
+async function update(inc: any) {
+ const stats = await getTodayStats();
+
+ await Stats.findOneAndUpdate({
+ _id: stats._id
+ }, {
+ $inc: inc
+ });
+}
+
+export async function updateUserStats(user: IUser, isAdditional: boolean) {
+ const inc = {} as any;
+
+ const amount = isAdditional ? 1 : -1;
+
+ if (isLocalUser(user)) {
+ inc['users.local.total'] = amount;
+ inc['users.local.diff'] = amount;
+ } else {
+ inc['users.remote.total'] = amount;
+ inc['users.remote.diff'] = amount;
+ }
+
+ await update(inc);
+}
+
+export async function updateNoteStats(note: INote, isAdditional: boolean) {
+ const inc = {} as any;
+
+ const amount = isAdditional ? 1 : -1;
+
+ if (isLocalUser(note._user)) {
+ inc['notes.local.total'] = amount;
+ inc['notes.local.diff'] = amount;
+
+ if (note.replyId != null) {
+ inc['notes.local.diffs.reply'] = amount;
+ } else if (note.renoteId != null) {
+ inc['notes.local.diffs.renote'] = amount;
+ } else {
+ inc['notes.local.diffs.normal'] = amount;
+ }
+ } else {
+ inc['notes.remote.total'] = amount;
+ inc['notes.remote.diff'] = amount;
+
+ if (note.replyId != null) {
+ inc['notes.remote.diffs.reply'] = amount;
+ } else if (note.renoteId != null) {
+ inc['notes.remote.diffs.renote'] = amount;
+ } else {
+ inc['notes.remote.diffs.normal'] = amount;
+ }
+ }
+
+ await update(inc);
+}
+
+export async function updateDriveStats(file: IDriveFile, isAdditional: boolean) {
+ const inc = {} as any;
+
+ const amount = isAdditional ? 1 : -1;
+ const size = isAdditional ? file.length : -file.length;
+
+ if (isLocalUser(file.metadata._user)) {
+ inc['drive.local.totalCount'] = amount;
+ inc['drive.local.diffCount'] = amount;
+ inc['drive.local.totalSize'] = size;
+ inc['drive.local.diffSize'] = size;
+ } else {
+ inc['drive.remote.total'] = amount;
+ inc['drive.remote.diff'] = amount;
+ inc['drive.remote.totalSize'] = size;
+ inc['drive.remote.diffSize'] = size;
+ }
+
+ await update(inc);
+}