Improve admin dashboard

This commit is contained in:
syuilo 2018-11-03 11:38:00 +09:00
parent f9f2ca51ac
commit aadd5b95b8
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
10 changed files with 183 additions and 8 deletions

View file

@ -1082,6 +1082,8 @@ admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
accounts: "アカウント" accounts: "アカウント"
notes: "投稿" notes: "投稿"
drive: "ドライブ"
instances: "インスタンス"
this-instance: "このインスタンス" this-instance: "このインスタンス"
federated: "連合" federated: "連合"
invite: "招待" invite: "招待"

View file

@ -0,0 +1,113 @@
<template>
<div class="hyhctythnmwihguaaapnbrbszsjqxpio">
<table>
<thead>
<tr>
<th>%fa:exchange-alt% In/Out</th>
<th>%fa:server% Host</th>
<th>%fa:bolt% Activity</th>
<th>%fa:user% Actor</th>
</tr>
</thead>
<tbody>
<tr v-for="log in logs" :key="log.id">
<td :class="log.direction">{{ log.direction == 'in' ? '<' : '>' }} {{ log.direction }}</td>
<td>{{ log.host }}</td>
<td>{{ log.activity }}</td>
<td>@{{ log.actor }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
logs: [],
connection: null
};
},
mounted() {
this.connection = (this as any).os.stream.useSharedConnection('apLog');
this.connection.on('stats', this.onLog);
this.connection.on('statsLog', this.onLogs);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 50
});
setInterval(() => {
this.onLog({
direction: ['in', 'out'][Math.floor(Math.random() * 2)],
activity: 'Create',
host: 'misskey.ai',
actor: 'foobar'
});
}, 1000);
},
beforeDestroy() {
this.connection.dispose();
},
methods: {
onLog(log) {
log.id = Math.random();
this.logs.unshift(log);
if (this.logs.length > 50) this.logs.pop();
},
onLogs(logs) {
logs.reverse().forEach(log => this.onLog(log));
}
}
});
</script>
<style lang="stylus" scoped>
.hyhctythnmwihguaaapnbrbszsjqxpio
display block
padding 16px
height 250px
overflow auto
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
background var(--face)
border-radius 8px
> table
width 100%
max-width 100%
overflow auto
border-spacing 0
border-collapse collapse
color #555
thead
font-weight bold
border-bottom solid 2px #eee
tr
th
text-align left
tbody
tr
&:nth-child(odd)
background #fbfbfb
th, td
padding 8px 16px
min-width 128px
td.in
color #d26755
td.out
color #55bb83
</style>

View file

@ -7,6 +7,7 @@
<p><b>Node</b><span>{{ meta.node }}</span></p> <p><b>Node</b><span>{{ meta.node }}</span></p>
<p>藍ちゃかわいい</p> <p>藍ちゃかわいい</p>
</header> </header>
<div v-if="stats" class="stats"> <div v-if="stats" class="stats">
<div> <div>
<div> <div>
@ -34,22 +35,22 @@
</div> </div>
<div> <div>
<div> <div>
<div>%fa:user%</div> <div>%fa:database%</div>
<div> <div>
<span>%i18n:@accounts%</span> <span>%i18n:@drive%</span>
<b>{{ stats.usersCount | number }}</b> <b>{{ stats.driveUsageLocal | bytes }}</b>
</div> </div>
</div> </div>
<div> <div>
<span>%fa:globe% %i18n:@federated%</span> <span>%fa:home% %i18n:@this-instance%</span>
</div> </div>
</div> </div>
<div> <div>
<div> <div>
<div>%fa:pencil-alt%</div> <div>%fa:hdd R%</div>
<div> <div>
<span>%i18n:@notes%</span> <span>%i18n:@instances%</span>
<b>{{ stats.notesCount | number }}</b> <b>{{ stats.instances | number }}</b>
</div> </div>
</div> </div>
<div> <div>
@ -65,6 +66,10 @@
<div class="cpu-memory"> <div class="cpu-memory">
<x-cpu-memory :connection="connection"/> <x-cpu-memory :connection="connection"/>
</div> </div>
<div class="ap">
<x-ap-log/>
</div>
</div> </div>
</template> </template>
@ -72,11 +77,13 @@
import Vue from "vue"; import Vue from "vue";
import XCpuMemory from "./cpu-memory.vue"; import XCpuMemory from "./cpu-memory.vue";
import XCharts from "./charts.vue"; import XCharts from "./charts.vue";
import XApLog from "./ap-log.vue";
export default Vue.extend({ export default Vue.extend({
components: { components: {
XCpuMemory, XCpuMemory,
XCharts XCharts,
XApLog
}, },
data() { data() {
return { return {

View file

@ -1,6 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
Vue.filter('bytes', (v, digits = 0) => { Vue.filter('bytes', (v, digits = 0) => {
if (v == null) return '?';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (v == 0) return '0'; if (v == 0) return '0';
const isMinus = v < 0; const isMinus = v < 0;

View file

@ -1,5 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
Vue.filter('number', (n) => { Vue.filter('number', (n) => {
if (n == null) return 'N/A';
return n.toLocaleString(); return n.toLocaleString();
}); });

View file

@ -8,6 +8,7 @@ import perform from '../../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person'; import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import { URL } from 'url'; import { URL } from 'url';
import { publishApLogStream } from '../../../stream';
const log = debug('misskey:queue:inbox'); const log = debug('misskey:queue:inbox');
@ -61,6 +62,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
}) as IRemoteUser; }) as IRemoteUser;
} }
//#region Log
publishApLogStream({
direction: 'in',
activity: activity.type,
host: user.host,
actor: user.username
});
//#endregion
// Update activityの場合は、ここで署名検証/更新処理まで実施して終了 // Update activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') { if (activity.type === 'Update') {
if (activity.object && activity.object.type === 'Person') { if (activity.object && activity.object.type === 'Person') {

View file

@ -6,6 +6,7 @@ const crypto = require('crypto');
import config from '../../config'; import config from '../../config';
import { ILocalUser } from '../../models/user'; import { ILocalUser } from '../../models/user';
import { publishApLogStream } from '../../stream';
const log = debug('misskey:activitypub:deliver'); const log = debug('misskey:activitypub:deliver');
@ -64,4 +65,13 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
}); });
req.end(data); req.end(data);
//#region Log
publishApLogStream({
direction: 'out',
activity: object.type,
host: null,
actor: user.username
});
//#endregion
}); });

View file

@ -0,0 +1,24 @@
import autobind from 'autobind-decorator';
import Channel from '../channel';
export default class extends Channel {
public readonly chName = 'apLog';
public static shouldShare = true;
@autobind
public async init(params: any) {
// Subscribe events
this.subscriber.on('apLog', this.onLog);
}
@autobind
private async onLog(log: any) {
this.send('log', log);
}
@autobind
public dispose() {
// Unsubscribe events
this.subscriber.off('apLog', this.onLog);
}
}

View file

@ -10,6 +10,7 @@ import messaging from './messaging';
import messagingIndex from './messaging-index'; import messagingIndex from './messaging-index';
import drive from './drive'; import drive from './drive';
import hashtag from './hashtag'; import hashtag from './hashtag';
import apLog from './ap-log';
import gamesReversi from './games/reversi'; import gamesReversi from './games/reversi';
import gamesReversiGame from './games/reversi-game'; import gamesReversiGame from './games/reversi-game';
@ -26,6 +27,7 @@ export default {
messagingIndex, messagingIndex,
drive, drive,
hashtag, hashtag,
apLog,
gamesReversi, gamesReversi,
gamesReversiGame gamesReversiGame
}; };

View file

@ -100,6 +100,10 @@ class Publisher {
public publishHashtagStream = (note: any): void => { public publishHashtagStream = (note: any): void => {
this.publish('hashtag', null, note); this.publish('hashtag', null, note);
} }
public publishApLogStream = (log: any): void => {
this.publish('apLog', null, log);
}
} }
const publisher = new Publisher(); const publisher = new Publisher();
@ -119,3 +123,4 @@ export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
export const publishHybridTimelineStream = publisher.publishHybridTimelineStream; export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
export const publishHashtagStream = publisher.publishHashtagStream; export const publishHashtagStream = publisher.publishHashtagStream;
export const publishApLogStream = publisher.publishApLogStream;