Merge branch 'develop' into swn
This commit is contained in:
commit
6d9e83989c
49 changed files with 1157 additions and 2275 deletions
|
@ -13,10 +13,12 @@
|
|||
## 12.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
- クライアント: コントロールパネルのパフォーマンスを改善
|
||||
- クライアント: 自分のリアクション一覧を見れるように
|
||||
- 設定により、リアクション一覧を全員に公開することも可能
|
||||
- クライアント: ユーザー検索の精度を強化
|
||||
- クライアント: 新しいライトテーマを追加
|
||||
- クライアント: 新しいダークテーマを追加
|
||||
- API: ユーザーのリアクション一覧を取得する users/reactions を追加
|
||||
- API: users/search および users/search-by-username-and-host を強化
|
||||
- ミュート及びブロックのインポートを行えるように
|
||||
|
@ -25,6 +27,7 @@
|
|||
### Bugfixes
|
||||
- クライアント: テーマの管理が行えない問題を修正
|
||||
- API: アプリケーション通知が取得できない問題を修正
|
||||
- クライアント: リモートノートで意図せずローカルカスタム絵文字が使われてしまうことがあるのを修正
|
||||
- ActivityPub: not reacted な Undo.Like がinboxに滞留するのを修正
|
||||
|
||||
## 12.92.0 (2021/10/16)
|
||||
|
|
|
@ -65,7 +65,7 @@ Organize and store your files! Want to post a picture you have already uploaded?
|
|||
|
||||
:package: Create your own instance
|
||||
----------------------------------------------------------------
|
||||
Please see the [Setup and Installation Guide](./docs/setup.en.md).
|
||||
Please see the [Setup and Installation Guide](https://misskey-hub.net/docs/install/install.html).
|
||||
|
||||
:wrench: Contribution
|
||||
----------------------------------------------------------------
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
# Docs
|
||||
These docs are for contributors of Misskey or admins of instance of Misskey.
|
||||
Docs for users are located in `src/docs`.
|
||||
|
||||
これらのドキュメントはMisskeyの開発者またはMisskeyインスタンス運営者向けです。
|
||||
利用者向けのドキュメントは`src/docs`にあります。
|
||||
|
||||
这些文档是为 Misskey 的贡献者,或是 Misskey 实例的管理者准备的。
|
||||
为用户准备的文档放置在 `src/docs` 文件夹中。
|
||||
|
||||
## 日本語版
|
||||
|
||||
- [Misskey構築の手引き](./setup.ja.md)
|
||||
- [運営ガイド](./manage.ja.md)
|
||||
- [Dockerを使ったMisskey構築方法](./docker.ja.md)
|
||||
|
||||
## English Version
|
||||
|
||||
- [Misskey Setup and Installation Guide](./setup.en.md)
|
||||
- [Management guide](./manage.en.md)
|
||||
- [Docker Guide](./docker.en.md)
|
||||
|
||||
## Française Version
|
||||
|
||||
- [Guide d'installation et de configuration de Misskey](./setup.fr.md)
|
||||
- [Guide d'administration](./manage.fr.md)
|
||||
- [Guide Docker](./docker.fr.md)
|
||||
|
||||
## 简体中文版
|
||||
|
||||
- [Misskey 设置和安装指南](./setup.zh.md)
|
||||
- [运营指南](./manage.zh.md)
|
||||
- [Docker 部署指南](./docker.zh.md)
|
|
@ -1,97 +0,0 @@
|
|||
Docker Guide
|
||||
================================================================
|
||||
|
||||
This guide describes how to install and setup Misskey with Docker.
|
||||
|
||||
- [Japanese version also available - 日本語版もあります](./docker.ja.md)
|
||||
- [Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Download Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Clone Misskey repository's master branch.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. Move to misskey directory.
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. Checkout to the [latest release](https://github.com/misskey-dev/misskey/releases/latest) tag.
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* Configure Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
Create configuration files with following:
|
||||
|
||||
```bash
|
||||
cd .config
|
||||
cp example.yml default.yml
|
||||
cp docker_example.env docker.env
|
||||
```
|
||||
|
||||
### `default.yml`
|
||||
|
||||
Edit this file the same as non-Docker environment.
|
||||
However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`.
|
||||
The following is default hostname:
|
||||
|
||||
| Service | Hostname |
|
||||
|---------------|----------|
|
||||
| Postgresql | `db` |
|
||||
| Redis | `redis` |
|
||||
| Elasticsearch | `es` |
|
||||
|
||||
### `docker.env`
|
||||
|
||||
Configure Postgresql in this file.
|
||||
The minimum required settings are:
|
||||
|
||||
| name | Description |
|
||||
|---------------------|---------------|
|
||||
| `POSTGRES_PASSWORD` | Password |
|
||||
| `POSTGRES_USER` | Username |
|
||||
| `POSTGRES_DB` | Database name |
|
||||
|
||||
*3.* Configure Docker
|
||||
----------------------------------------------------------------
|
||||
Edit `docker-compose.yml`.
|
||||
|
||||
*4.* Build Misskey
|
||||
----------------------------------------------------------------
|
||||
Build misskey with the following:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* Init DB
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
docker-compose run --rm web yarn run init
|
||||
```
|
||||
|
||||
*6.* That is it.
|
||||
----------------------------------------------------------------
|
||||
Well done! Now you have an environment to run Misskey.
|
||||
|
||||
### Launch normally
|
||||
Just `docker-compose up -d`. GLHF!
|
||||
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. Check [ChangeLog](../CHANGELOG.md) for migration information
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### How to execute [cli commands](manage.en.md):
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
If you have any questions or trouble, feel free to contact us!
|
|
@ -1,91 +0,0 @@
|
|||
Guide Docker
|
||||
================================================================
|
||||
|
||||
Ce guide explique comment installer et configurer Misskey avec Docker.
|
||||
|
||||
- [Version japonaise également disponible - Japanese version also available - 日本語版もあります](./docker.ja.md)
|
||||
- [Version anglaise également disponible - English version also available - 英語版もあります](./docker.en.md)
|
||||
- [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Télécharger Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Clone le dépôt de Misskey sur la branche master.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. Naviguez dans le dossier du dépôt.
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. Checkout sur le tag de la [dernière version](https://github.com/misskey-dev/misskey/releases/latest).
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* Configuration de Misskey
|
||||
----------------------------------------------------------------
|
||||
1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le `default.yml`.
|
||||
2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copie le fichier `.config/mongo_initdb_example.js` et le renomme en `mongo_initdb.js`.
|
||||
3. Editez `default.yml` et `mongo_initdb.js`.
|
||||
|
||||
*3.* Configurer Docker
|
||||
----------------------------------------------------------------
|
||||
Editez `docker-compose.yml`.
|
||||
|
||||
*4.* Contruire Misskey
|
||||
----------------------------------------------------------------
|
||||
Contruire l'image Docker avec:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* C'est tout !
|
||||
----------------------------------------------------------------
|
||||
Parfait, Vous avez un environnement prêt pour démarrer Misskey.
|
||||
|
||||
### Lancer normalement
|
||||
Utilisez la commande `docker-compose up -d`. GLHF!
|
||||
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. Consultez le [ChangeLog](../CHANGELOG.md) pour avoir les éventuelles informations de migration
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### Comment exécuter des [commandes](manage.fr.md)
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
### Configuration d'ElasticSearch (pour la fonction de recherche)
|
||||
*1.* Préparation de l'environnement
|
||||
----------------------------------------------------------------
|
||||
1. Permet de créer le dossier d'accueil de la base ElasticSearch aves les bons droits
|
||||
|
||||
`mkdir elasticsearch && chown 1000:1000 elasticsearch`
|
||||
|
||||
2. Augmente la valeur max du paramètre map_count du système (valeur minimum pour pouvoir lancer ES)
|
||||
|
||||
`sysctl -w vm.max_map_count=262144`
|
||||
|
||||
*2.* Après lancement du docker-compose, initialisation de la base ElasticSearch
|
||||
----------------------------------------------------------------
|
||||
1. Connexion dans le conteneur web
|
||||
|
||||
`docker-compose -it web /bin/sh`
|
||||
|
||||
2. Ajout du paquet curl
|
||||
|
||||
`apk add curl`
|
||||
|
||||
3. Création de la base ES
|
||||
|
||||
`curl -X PUT "es:9200/misskey" -H 'Content-Type: application/json' -d'{ "settings" : { "index" : { } }}'`
|
||||
|
||||
4. `exit`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
Si vous avez des questions ou des problèmes, n'hésitez pas à nous contacter !
|
|
@ -1,98 +0,0 @@
|
|||
Dockerを使ったMisskey構築方法
|
||||
================================================================
|
||||
|
||||
このガイドはDockerを使ったMisskeyセットアップ方法について解説します。
|
||||
|
||||
- [英語版もあります - English version also available](./docker.en.md)
|
||||
- [简体中文版同样可用 - Simplified Chinese version also available](./docker.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Misskeyのダウンロード
|
||||
----------------------------------------------------------------
|
||||
1. masterブランチからMisskeyレポジトリをクローン
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. misskeyディレクトリに移動
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. [最新のリリース](https://github.com/misskey-dev/misskey/releases/latest)を確認
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* 設定ファイルの作成と編集
|
||||
----------------------------------------------------------------
|
||||
|
||||
下記コマンドで設定ファイルを作成してください。
|
||||
|
||||
```bash
|
||||
cd .config
|
||||
cp example.yml default.yml
|
||||
cp docker_example.env docker.env
|
||||
```
|
||||
|
||||
### `default.yml`の編集
|
||||
|
||||
非Docker環境と同じ様に編集してください。
|
||||
ただし、Postgresql、RedisとElasticsearchのホストは`localhost`ではなく、`docker-compose.yml`で設定されたサービス名になっています。
|
||||
標準設定では次の通りです。
|
||||
|
||||
| サービス | ホスト名 |
|
||||
|---------------|---------|
|
||||
| Postgresql |`db` |
|
||||
| Redis |`redis` |
|
||||
| Elasticsearch |`es` |
|
||||
|
||||
### `docker.env`の編集
|
||||
|
||||
このファイルはPostgresqlの設定を記述します。
|
||||
最低限記述する必要がある設定は次の通りです。
|
||||
|
||||
| 設定 | 内容 |
|
||||
|---------------------|--------------|
|
||||
| `POSTGRES_PASSWORD` | パスワード |
|
||||
| `POSTGRES_USER` | ユーザー名 |
|
||||
| `POSTGRES_DB` | データベース名 |
|
||||
|
||||
*3.* Dockerの設定
|
||||
----------------------------------------------------------------
|
||||
`docker-compose.yml`を編集してください。
|
||||
|
||||
*4.* Misskeyのビルド
|
||||
----------------------------------------------------------------
|
||||
次のコマンドでMisskeyをビルドしてください:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* データベースを初期化
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
docker-compose run --rm web yarn run init
|
||||
```
|
||||
|
||||
*6.* 以上です!
|
||||
----------------------------------------------------------------
|
||||
お疲れ様でした。これでMisskeyを動かす準備は整いました。
|
||||
|
||||
### 通常起動
|
||||
`docker-compose up -d`するだけです。GLHF!
|
||||
|
||||
### Misskeyを最新バージョンにアップデートする方法:
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### cliコマンドを実行する方法:
|
||||
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
なにかお困りのことがありましたらお気軽にご連絡ください。
|
|
@ -1,97 +0,0 @@
|
|||
Docker 部署指南
|
||||
================================================================
|
||||
|
||||
这份指南描述了如何使用Docker安装并设置 Misskey 。
|
||||
|
||||
- [日本語版もあります - Japanese version also available](./docker.ja.md)
|
||||
- [英語版もあります - English version also available](./docker.en.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* 下载 Misskey
|
||||
----------------------------------------------------------------
|
||||
1. 克隆 Misskey 项目的 master 分支。
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. 进入 misskey 文件夹。
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. 检查 [最新发布版](https://github.com/misskey-dev/misskey/releases/latest) 标签。
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* 配置 Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
可以按照如下方式创建配置文件:
|
||||
|
||||
``` bash
|
||||
cd .config
|
||||
cp example.yml default.yml
|
||||
cp docker_example.env docker.env
|
||||
```
|
||||
|
||||
### `default.yml`
|
||||
|
||||
这个文件的编辑工作基本与非 Docker 环境的版本相同。
|
||||
但请注意, Postgresql、 Redis 和 Elasticsearch 的 **主机名(hostname)** 配置不应该是 `localhost` ,它们被设置在 `docker-compose.yml` 文件中。
|
||||
以下是默认的主机名:
|
||||
|
||||
| 服务 | 主机名 |
|
||||
|---------------|----------|
|
||||
| Postgresql | `db` |
|
||||
| Redis | `redis` |
|
||||
| Elasticsearch | `es` |
|
||||
|
||||
### `docker.env`
|
||||
|
||||
在这个文件中配置 Postgresql 。
|
||||
至少需要如下这些配置:
|
||||
|
||||
| 名称 | 描述 |
|
||||
|---------------------|---------------|
|
||||
| `POSTGRES_PASSWORD` | 数据库密码 |
|
||||
| `POSTGRES_USER` | 数据库用户名 |
|
||||
| `POSTGRES_DB` | 数据库名 |
|
||||
|
||||
*3.* 配置 Docker
|
||||
----------------------------------------------------------------
|
||||
编辑 `docker-compose.yml` 文件。
|
||||
|
||||
*4.* 构建 Misskey
|
||||
----------------------------------------------------------------
|
||||
使用如下的方式构建Misskey:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* 初始化数据库
|
||||
----------------------------------------------------------------
|
||||
``` bash
|
||||
docker-compose run --rm web yarn run init
|
||||
```
|
||||
|
||||
*6.* 完成了!
|
||||
----------------------------------------------------------------
|
||||
干得不错!现在您拥有了一个可以运行Misskey的环境啦。
|
||||
|
||||
### 正常启动
|
||||
只需要 `docker-compose up -d` 即可。玩得愉快!
|
||||
|
||||
### 如何将您的 Misskey 服务器升级至最新版本
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. 检查 [更新日志](../CHANGELOG.md) 以获取升级迁移信息。
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### 如何执行 [控制台指令](manage.zh.md):
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
如果您有任何疑问或是困惑,欢迎与我们联系!
|
|
@ -1,71 +0,0 @@
|
|||
# Sample nginx configuration for Misskey
|
||||
#
|
||||
# 1. Replace example.tld to your domain
|
||||
# 2. Copy to /etc/nginx/sites-available/ and then symlink from /etc/nginx/sites-enabled/
|
||||
# or copy to /etc/nginx/conf.d/
|
||||
|
||||
# For WebSocket
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.tld;
|
||||
|
||||
# For SSL domain validation
|
||||
root /var/www/html;
|
||||
location /.well-known/acme-challenge/ { allow all; }
|
||||
location /.well-known/pki-validation/ { allow all; }
|
||||
location / { return 301 https://$server_name$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name example.tld;
|
||||
ssl_session_cache shared:ssl_session_cache:10m;
|
||||
|
||||
# To use Let's Encrypt certificate
|
||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
|
||||
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
|
||||
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||
|
||||
# SSL protocol settings
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES128-SHA;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# Change to your upload limit
|
||||
client_max_body_size 80m;
|
||||
|
||||
# Proxy to Node
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
|
||||
# If it's behind another reverse proxy or CDN, remove the following.
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# For WebSocket
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Cache settings
|
||||
proxy_cache cache1;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_use_stale updating;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
# Management guide
|
||||
|
||||
## Check the status of the job queue
|
||||
coming soon
|
||||
|
||||
## Mark as 'admin' user
|
||||
``` shell
|
||||
node built/tools/mark-admin (Username)
|
||||
```
|
||||
|
||||
e.g.
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
|
@ -1,14 +0,0 @@
|
|||
# Guide d'administration
|
||||
|
||||
## Vérifier le status de la file d'attente des taches
|
||||
coming soon
|
||||
|
||||
## Marquer un utilisateur en tant que 'admin'
|
||||
``` shell
|
||||
node built/tools/mark-admin (nom d'utilisateur)
|
||||
```
|
||||
|
||||
Exemple :
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
|
@ -1,14 +0,0 @@
|
|||
# 運営ガイド
|
||||
|
||||
## ジョブキューの状態を調べる
|
||||
coming soon
|
||||
|
||||
## 管理者ユーザーを設定する
|
||||
``` shell
|
||||
node built/tools/mark-admin (ユーザー名)
|
||||
```
|
||||
|
||||
例:
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
|
@ -1,14 +0,0 @@
|
|||
# 运营指南
|
||||
|
||||
## 检查任务队列的状态
|
||||
即将到来……
|
||||
|
||||
## 设置用户为管理员
|
||||
``` shell
|
||||
node built/tools/mark-admin (用户名)
|
||||
```
|
||||
|
||||
样例
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
|
@ -1,28 +0,0 @@
|
|||
GitHub Actionsを使用してDocker Hubへpushする方法
|
||||
================================================================
|
||||
|
||||
[/.github/workflows/docker.yml](/.github/workflows/docker.yml) に
|
||||
GitHub ActionによりDocker Hubへpushするワークフローが記述されています。
|
||||
|
||||
オリジナルリポジトリでは、リリースされたタイミングで `latest`, `<リリース名>` それぞれのタグでDocker Hubにpushされます。
|
||||
※ Docker Hub に`<ブランチ名>`のようなタグがあるかもしれませんが、こちらは自動push対象ではありません。
|
||||
|
||||
Fork先でこのワークフローを実行すると失敗します。
|
||||
|
||||
以下では、Fork先で自分のDocker Hubリポジトリにpushするようにする方法を記述します。
|
||||
|
||||
## 自分のDocker Hubリポジトリにpushするように設定する方法
|
||||
|
||||
1. Docker Hubでリポジトリを作成します。
|
||||
2. ワークフローファイルの [images](https://github.com/misskey-dev/misskey/blob/53f3b779bf16abcda4f6e026c51384f3b8fbcc62/.github/workflows/docker.yml#L20) を作成したリポジトリに置き換えます。
|
||||
3. GitHubにて [暗号化されたシークレット](https://docs.github.com/ja/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) を作成します。
|
||||
作成が必要なのは `DOCKER_USERNAME` と `DOCKER_PASSWORD` で、それぞれDocker Hubのユーザーとパスワードになります。
|
||||
|
||||
## pushする方法
|
||||
|
||||
上記設定によりリリース時に自動的にDocker Hubにpushされるようになります。
|
||||
具体的には、GitHubのリリース機能でリリースしたタイミングで `latest`, `<リリース名>` それぞれのタグでDocker Hubにpushされます。
|
||||
|
||||
また、GitHub上から手動でpushすることも出来ます。
|
||||
それを行うには、Actions => Publish Docker image => Run workflow からbranchを選択してワークフローを実行します。
|
||||
ただし、この場合作成されるタグは`<ブランチ名>`になります。
|
147
docs/setup.en.md
147
docs/setup.en.md
|
@ -1,147 +0,0 @@
|
|||
Misskey Setup and Installation Guide
|
||||
================================================================
|
||||
|
||||
We thank you for your interest in setting up your Misskey server!
|
||||
This guide describes how to install and setup Misskey.
|
||||
|
||||
- [Japanese version also available - 日本語版もあります](./setup.ja.md)
|
||||
- [Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Create Misskey user
|
||||
----------------------------------------------------------------
|
||||
Running misskey as root is not a good idea so we create a user for that.
|
||||
In debian for exemple :
|
||||
|
||||
```
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* Install dependencies
|
||||
----------------------------------------------------------------
|
||||
Please install and setup these softwares:
|
||||
|
||||
#### Dependencies :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (12.x / 13.x is preferred)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optional
|
||||
* [Yarn](https://yarnpkg.com/) *Optional but recommended for security reason. If you won't install it, use `npx yarn` instead of `yarn`.*
|
||||
* [Elasticsearch](https://www.elastic.co/) - required to enable the search feature
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* Install Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Connect to misskey user.
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. Clone the misskey repo from master branch.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. Navigate to misskey directory
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. Checkout to the [latest release](https://github.com/misskey-dev/misskey/releases/latest)
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. Install misskey dependencies.
|
||||
|
||||
`yarn`
|
||||
|
||||
*4.* Configure Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Copy the `.config/example.yml` and rename it to `default.yml`.
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. Edit `default.yml`
|
||||
|
||||
*5.* Build Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
Build misskey with the following:
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
If you're on Debian, you will need to install the `build-essential`, `python` package.
|
||||
|
||||
If you're still encountering errors about some modules, use node-gyp:
|
||||
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* Init DB
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
yarn run init
|
||||
```
|
||||
|
||||
*7.* That is it.
|
||||
----------------------------------------------------------------
|
||||
Well done! Now, you have an environment that run to Misskey.
|
||||
|
||||
### Launch normally
|
||||
Just `NODE_ENV=production npm start`. GLHF!
|
||||
|
||||
### Launch with systemd
|
||||
|
||||
1. Create a systemd service here
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. Edit it, and paste this and save:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. Reload systemd and enable the misskey service.
|
||||
|
||||
`systemctl daemon-reload ; systemctl enable misskey`
|
||||
|
||||
4. Start the misskey service.
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
You can check if the service is running with `systemctl status misskey`.
|
||||
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
7. Restart your Misskey process to apply changes
|
||||
8. Enjoy
|
||||
|
||||
If you encounter any problems with updating, please try the following:
|
||||
1. `yarn clean` or `yarn cleanall`
|
||||
2. Retry update (Don't forget `yarn install`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
If you have any questions or troubles, feel free to contact us!
|
136
docs/setup.fr.md
136
docs/setup.fr.md
|
@ -1,136 +0,0 @@
|
|||
Guide d'installation et de configuration de Misskey
|
||||
================================================================
|
||||
|
||||
Nous vous remerçions de l'intrêt que vous manifestez pour l'installation de votre propre instance Misskey !
|
||||
Ce guide décrit les étapes à suivre afin d'installer et de configurer une instance Misskey.
|
||||
|
||||
- [La version en japonnais est également disponible sur - 日本語版もあります](./setup.ja.md)
|
||||
- [Version anglaise également disponible - English version also available - 英語版もあります](./setup.en.md)
|
||||
- [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Création de l'utilisateur Misskey
|
||||
----------------------------------------------------------------
|
||||
Executer misskey en tant que super-utilisateur étant une mauvaise idée, nous allons créer un utilisateur dédié.
|
||||
Sous Debian, par exemple :
|
||||
|
||||
```
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* Installation des dépendances
|
||||
----------------------------------------------------------------
|
||||
Installez les paquets suivants :
|
||||
|
||||
#### Dépendences :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (>= 10)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optionnels
|
||||
* [Yarn](https://yarnpkg.com/) - *recommander pour des raisons de sécurité. Si vous ne l'installez pas, utilisez `npx yarn` au lieu de` yarn`.*
|
||||
* [Elasticsearch](https://www.elastic.co/) - *requis pour pouvoir activer la fonctionnalité de recherche.*
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* Installation de Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Basculez vers l'utilisateur misskey.
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. Clonez la branche master du dépôt misskey.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. Accédez au dossier misskey.
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. Checkout sur le tag de la [version la plus récente](https://github.com/misskey-dev/misskey/releases/latest)
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. Installez les dépendances de misskey.
|
||||
|
||||
`yarn install`
|
||||
|
||||
*4.* Création du fichier de configuration
|
||||
----------------------------------------------------------------
|
||||
1. Copiez le fichier `.config/example.yml` et renommez-le`default.yml`.
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. Editez le fichier `default.yml`
|
||||
|
||||
*5.* Construction de Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
Construisez Misskey comme ceci :
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
Si vous êtes sous Debian, vous serez amené à installer les paquets `build-essential` et `python`.
|
||||
|
||||
Si vous rencontrez des erreurs concernant certains modules, utilisez node-gyp:
|
||||
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* C'est tout.
|
||||
----------------------------------------------------------------
|
||||
Excellent ! Maintenant, vous avez un environnement prêt pour lancer Misskey
|
||||
|
||||
### Lancement conventionnel
|
||||
Lancez tout simplement `NODE_ENV=production yarn start`. Bonne chance et amusez-vous bien !
|
||||
|
||||
### Démarrage avec systemd
|
||||
|
||||
1. Créez un service systemd sur
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. Editez-le puis copiez et coller ceci dans le fichier :
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. Redémarre systemd et active le service misskey.
|
||||
|
||||
`systemctl daemon-reload ; systemctl enable misskey`
|
||||
|
||||
4. Démarre le service misskey.
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
Vous pouvez vérifier si le service a démarré en utilisant la commande `systemctl status misskey`.
|
||||
|
||||
### Méthode de mise à jour vers la plus récente version de Misskey
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
Si vous rencontrez des difficultés ou avez d'autres questions, n'hésitez pas à nous contacter !
|
145
docs/setup.ja.md
145
docs/setup.ja.md
|
@ -1,145 +0,0 @@
|
|||
Misskey構築の手引き
|
||||
================================================================
|
||||
|
||||
Misskeyサーバーの構築にご関心をお寄せいただきありがとうございます!
|
||||
このガイドではMisskeyのインストール・セットアップ方法について解説します。
|
||||
|
||||
- [英語版もあります - English version also available](./setup.en.md)
|
||||
- [简体中文版同样可用 - Simplified Chinese version also available](./setup.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Misskeyユーザーの作成
|
||||
----------------------------------------------------------------
|
||||
Misskeyはrootユーザーで実行しない方がよいため、代わりにユーザーを作成します。
|
||||
Debianの例:
|
||||
|
||||
```
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* 依存関係をインストールする
|
||||
----------------------------------------------------------------
|
||||
これらのソフトウェアをインストール・設定してください:
|
||||
|
||||
#### 依存関係 :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (10以上)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### オプション
|
||||
* [Yarn](https://yarnpkg.com/)
|
||||
* セキュリティの観点から推奨されます。 yarn をインストールしない方針の場合は、文章中の `yarn` を適宜 `npx yarn` と読み替えてください。
|
||||
* [Elasticsearch](https://www.elastic.co/)
|
||||
* 検索機能を有効にするためにはインストールが必要です。
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* Misskeyのインストール
|
||||
----------------------------------------------------------------
|
||||
1. misskeyユーザーを使用
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. masterブランチからMisskeyレポジトリをクローン
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. misskeyディレクトリに移動
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. [最新のリリース](https://github.com/misskey-dev/misskey/releases/latest)を確認
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. Misskeyの依存パッケージをインストール
|
||||
|
||||
`yarn install`
|
||||
|
||||
*4.* 設定ファイルを作成する
|
||||
----------------------------------------------------------------
|
||||
1. `.config/example.yml`をコピーし名前を`default.yml`にする。
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. `default.yml` を編集する。
|
||||
|
||||
*5.* Misskeyのビルド
|
||||
----------------------------------------------------------------
|
||||
|
||||
次のコマンドでMisskeyをビルドしてください:
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
Debianをお使いであれば、`build-essential`パッケージをインストールする必要があります。
|
||||
|
||||
何らかのモジュールでエラーが発生する場合はnode-gypを使ってください:
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* データベースを初期化
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
yarn run init
|
||||
```
|
||||
|
||||
*7.* 以上です!
|
||||
----------------------------------------------------------------
|
||||
お疲れ様でした。これでMisskeyを動かす準備は整いました。
|
||||
|
||||
### 通常起動
|
||||
`NODE_ENV=production yarn start`するだけです。GLHF!
|
||||
|
||||
### systemdを用いた起動
|
||||
1. systemdサービスのファイルを作成
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. エディタで開き、以下のコードを貼り付けて保存:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。
|
||||
|
||||
3. systemdを再読み込みしmisskeyサービスを有効化
|
||||
|
||||
`systemctl daemon-reload; systemctl enable misskey`
|
||||
|
||||
4. misskeyサービスの起動
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
`systemctl status misskey`と入力すると、サービスの状態を調べることができます。
|
||||
|
||||
### Misskeyを最新バージョンにアップデートする方法:
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
|
||||
なにか問題が発生した場合は、`yarn clean`または`yarn cleanall`すると直る場合があります。
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
なにかお困りのことがありましたらお気軽にご連絡ください。
|
147
docs/setup.zh.md
147
docs/setup.zh.md
|
@ -1,147 +0,0 @@
|
|||
Misskey 设置和安装指南
|
||||
================================================================
|
||||
|
||||
非常感谢您对构建 Misskey 服务器的关注!
|
||||
这份指南描述了 Misskey 的安装与设置流程。
|
||||
|
||||
- [日本語版もあります - Japanese version also available](./setup.ja.md)
|
||||
- [英語版もあります - English version also available](./setup.en.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* 创建 Misskey 用户
|
||||
----------------------------------------------------------------
|
||||
直接使用 root 用户来运行 misskey 也许并不是一个好主意,因此我们有必要创建一个专用的用户。
|
||||
以 Debian 为例:
|
||||
|
||||
``` bash
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* 安装依赖
|
||||
----------------------------------------------------------------
|
||||
请安装并设置如下这些软件:
|
||||
|
||||
#### Dependencies :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (>= 10)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optional
|
||||
* [Yarn](https://yarnpkg.com/) *可选,但出于安全因素考虑还是推荐安装。如果您没有安装, 您需要使用 `npx yarn` 来代替 `yarn`.*
|
||||
* [Elasticsearch](https://www.elastic.co/) - 为了启用搜索功能,这个搜索引擎是有必要的。
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* 安装 Misskey
|
||||
----------------------------------------------------------------
|
||||
1. 连接至 misskey 用户.
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. 克隆 Misskey 项目的 master 分支。
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. 进入 misskey 文件夹。
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. 检查 [最新发布版](https://github.com/misskey-dev/misskey/releases/latest) 标签。
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. 安装 Misskey 的依赖。
|
||||
|
||||
`yarn`
|
||||
|
||||
*4.* 配置 Misskey
|
||||
----------------------------------------------------------------
|
||||
1. 复制 `.config/example.yml` 并重命名为 `default.yml`。
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. 编辑 `default.yml`
|
||||
|
||||
*5.* 构建 Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
使用如下的指令构建 Misskey :
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
如果您使用的是 Debian , 您需要安装 `build-essential`, `python` 环境包。
|
||||
|
||||
如果您仍然遇到有关某些模块的错误,您可以使用 node-gyp:
|
||||
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* 初始化数据库
|
||||
----------------------------------------------------------------
|
||||
``` bash
|
||||
yarn run init
|
||||
```
|
||||
|
||||
*7.* 完成了!
|
||||
----------------------------------------------------------------
|
||||
干得不错!现在您拥有了一个可以运行Misskey的环境啦。
|
||||
|
||||
### 正常启动
|
||||
只需要 `NODE_ENV=production npm start` 即可。玩得愉快!
|
||||
|
||||
### 使用 systemd 来启动
|
||||
|
||||
1. 在此处创建一个 systemd 服务:
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. 编辑它,粘贴如下内容并保存:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. 重启 systemd 并设置 misskey 服务自动启动:
|
||||
|
||||
`systemctl daemon-reload ; systemctl enable misskey`
|
||||
|
||||
4. 启动 misskey 服务:
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
您可以使用 `systemctl status misskey` 来检查服务是否正在运行。
|
||||
|
||||
### 如何将您的 Misskey 服务器升级至最新版本
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
7. 重启您的 Misskey 进程来应用改变。
|
||||
8. 尽情享受吧!
|
||||
|
||||
如果您在更新时遇到任何问题,请尝试以下操作:
|
||||
1. `yarn clean` 或是 `yarn cleanall`
|
||||
2. 重试升级 (请不要忘记 `yarn install` )
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
如果您有任何疑问或是困惑,欢迎与我们联系!
|
11
package.json
11
package.json
|
@ -60,6 +60,9 @@
|
|||
"@types/jsonld": "1.5.6",
|
||||
"@types/katex": "0.11.1",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/koa__cors": "3.0.3",
|
||||
"@types/koa__multer": "2.0.3",
|
||||
"@types/koa__router": "8.0.8",
|
||||
"@types/koa-bodyparser": "4.3.3",
|
||||
"@types/koa-cors": "0.0.2",
|
||||
"@types/koa-favicon": "2.0.21",
|
||||
|
@ -67,9 +70,6 @@
|
|||
"@types/koa-mount": "4.0.1",
|
||||
"@types/koa-send": "4.1.3",
|
||||
"@types/koa-views": "7.0.0",
|
||||
"@types/koa__cors": "3.0.3",
|
||||
"@types/koa__multer": "2.0.3",
|
||||
"@types/koa__router": "8.0.8",
|
||||
"@types/markdown-it": "12.2.3",
|
||||
"@types/matter-js": "0.17.5",
|
||||
"@types/mocha": "8.2.3",
|
||||
|
@ -119,7 +119,9 @@
|
|||
"cafy": "15.2.1",
|
||||
"cbor": "8.0.2",
|
||||
"chalk": "4.1.2",
|
||||
"chart.js": "2.9.4",
|
||||
"chart.js": "3.5.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-zoom": "1.1.1",
|
||||
"cli-highlight": "2.1.11",
|
||||
"compare-versions": "3.6.0",
|
||||
"concurrently": "6.3.0",
|
||||
|
@ -127,6 +129,7 @@
|
|||
"crc-32": "1.2.0",
|
||||
"css-loader": "6.4.0",
|
||||
"cssnano": "5.0.8",
|
||||
"date-fns": "2.25.0",
|
||||
"dateformat": "4.5.1",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "8.0.1",
|
||||
|
|
628
src/client/components/chart.vue
Normal file
628
src/client/components/chart.vue
Normal file
|
@ -0,0 +1,628 @@
|
|||
<template>
|
||||
<canvas ref="chartEl"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
} from 'chart.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
import * as os from '@client/os';
|
||||
import { defaultStore } from '@client/store';
|
||||
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle,
|
||||
Filler,
|
||||
zoomPlugin,
|
||||
);
|
||||
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
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})`;
|
||||
};
|
||||
|
||||
const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
|
||||
const getColor = (i) => {
|
||||
return colors[i % colors.length];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
args: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 90
|
||||
},
|
||||
span: {
|
||||
type: String as PropType<'hour' | 'day'>,
|
||||
required: true,
|
||||
},
|
||||
detailed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const now = new Date();
|
||||
let chartInstance: Chart = null;
|
||||
let data: {
|
||||
series: {
|
||||
name: string;
|
||||
type: 'line' | 'area';
|
||||
color?: string;
|
||||
borderDash?: number[];
|
||||
hidden?: boolean;
|
||||
data: {
|
||||
x: number;
|
||||
y: number;
|
||||
}[];
|
||||
}[];
|
||||
} = null;
|
||||
|
||||
const chartEl = ref<HTMLCanvasElement>(null);
|
||||
const fetching = ref(true);
|
||||
|
||||
const getDate = (ago: number) => {
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth();
|
||||
const d = now.getDate();
|
||||
const h = now.getHours();
|
||||
|
||||
return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
||||
};
|
||||
|
||||
const format = (arr) => {
|
||||
return arr.map((v, i) => ({
|
||||
x: getDate(i).getTime(),
|
||||
y: v
|
||||
}));
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
// フォントカラー
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
chartInstance = new Chart(chartEl.value, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: data.series.map((x, i) => ({
|
||||
parsing: false,
|
||||
label: x.name,
|
||||
data: x.data.slice().reverse(),
|
||||
pointRadius: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: x.color ? x.color : getColor(i),
|
||||
borderDash: x.borderDash || [],
|
||||
borderJoinStyle: 'round',
|
||||
backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
|
||||
fill: x.type === 'area',
|
||||
hidden: !!x.hidden,
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 8,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: props.span === 'day' ? 'month' : 'day',
|
||||
},
|
||||
grid: {
|
||||
display: props.detailed,
|
||||
color: gridColor,
|
||||
borderColor: 'rgb(0, 0, 0, 0)',
|
||||
},
|
||||
ticks: {
|
||||
display: props.detailed,
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
locale: enUS,
|
||||
},
|
||||
},
|
||||
min: getDate(props.limit).getTime(),
|
||||
},
|
||||
y: {
|
||||
position: 'left',
|
||||
grid: {
|
||||
color: gridColor,
|
||||
borderColor: 'rgb(0, 0, 0, 0)',
|
||||
},
|
||||
ticks: {
|
||||
display: props.detailed,
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
},
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true,
|
||||
},
|
||||
pinch: {
|
||||
enabled: true,
|
||||
},
|
||||
drag: {
|
||||
enabled: false,
|
||||
},
|
||||
mode: 'x',
|
||||
},
|
||||
limits: {
|
||||
x: {
|
||||
min: 'original',
|
||||
max: 'original',
|
||||
},
|
||||
y: {
|
||||
min: 'original',
|
||||
max: 'original',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const exportData = () => {
|
||||
// TODO
|
||||
};
|
||||
|
||||
const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Instances',
|
||||
type: 'area',
|
||||
data: format(total
|
||||
? raw.instance.total
|
||||
: sum(raw.instance.inc, negate(raw.instance.dec))
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchNotesChart = async (type: string): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
borderDash: [5, 5],
|
||||
data: format(type == 'combined'
|
||||
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
||||
: sum(raw[type].inc, negate(raw[type].dec))
|
||||
),
|
||||
}, {
|
||||
name: 'Renotes',
|
||||
type: 'area',
|
||||
data: format(type == 'combined'
|
||||
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
|
||||
: raw[type].diffs.renote
|
||||
),
|
||||
}, {
|
||||
name: 'Replies',
|
||||
type: 'area',
|
||||
data: format(type == 'combined'
|
||||
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
|
||||
: raw[type].diffs.reply
|
||||
),
|
||||
}, {
|
||||
name: 'Normal',
|
||||
type: 'area',
|
||||
data: format(type == 'combined'
|
||||
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
|
||||
: raw[type].diffs.normal
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchNotesTotalChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(sum(raw.local.total, raw.remote.total)),
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
data: format(raw.local.total),
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
data: format(raw.remote.total),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchUsersChart = async (total: boolean): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(total
|
||||
? sum(raw.local.total, raw.remote.total)
|
||||
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
||||
),
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
data: format(total
|
||||
? raw.local.total
|
||||
: sum(raw.local.inc, negate(raw.local.dec))
|
||||
),
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
data: format(total
|
||||
? raw.remote.total
|
||||
: sum(raw.remote.inc, negate(raw.remote.dec))
|
||||
),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchActiveUsersChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(sum(raw.local.users, raw.remote.users)),
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
data: format(raw.local.users),
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
data: format(raw.remote.users),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchDriveChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
borderDash: [5, 5],
|
||||
data: format(
|
||||
sum(
|
||||
raw.local.incSize,
|
||||
negate(raw.local.decSize),
|
||||
raw.remote.incSize,
|
||||
negate(raw.remote.decSize)
|
||||
)
|
||||
),
|
||||
}, {
|
||||
name: 'Local +',
|
||||
type: 'area',
|
||||
data: format(raw.local.incSize),
|
||||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
data: format(negate(raw.local.decSize)),
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
data: format(raw.remote.incSize),
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
data: format(negate(raw.remote.decSize)),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchDriveTotalChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
data: format(raw.local.totalSize),
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
data: format(raw.remote.totalSize),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchDriveFilesChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
borderDash: [5, 5],
|
||||
data: format(
|
||||
sum(
|
||||
raw.local.incCount,
|
||||
negate(raw.local.decCount),
|
||||
raw.remote.incCount,
|
||||
negate(raw.remote.decCount)
|
||||
)
|
||||
),
|
||||
}, {
|
||||
name: 'Local +',
|
||||
type: 'area',
|
||||
data: format(raw.local.incCount),
|
||||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
data: format(negate(raw.local.decCount)),
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
data: format(raw.remote.incCount),
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
data: format(negate(raw.remote.decCount)),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
data: format(raw.local.totalCount),
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
data: format(raw.remote.totalCount),
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'In',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(raw.requests.received)
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: format(raw.requests.succeeded)
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: format(raw.requests.failed)
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Users',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.users.total
|
||||
: sum(raw.users.inc, negate(raw.users.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Notes',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.notes.total
|
||||
: sum(raw.notes.inc, negate(raw.notes.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Following',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.following.total
|
||||
: sum(raw.following.inc, negate(raw.following.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Followers',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: format(total
|
||||
? raw.followers.total
|
||||
: sum(raw.followers.inc, negate(raw.followers.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Drive usage',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.drive.totalUsage
|
||||
: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
|
||||
)
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => {
|
||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||
return {
|
||||
series: [{
|
||||
name: 'Drive files',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: format(total
|
||||
? raw.drive.totalFiles
|
||||
: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
|
||||
)
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const fetchAndRender = async () => {
|
||||
const fetchData = () => {
|
||||
switch (props.src) {
|
||||
case 'federation-instances': return fetchFederationInstancesChart(false);
|
||||
case 'federation-instances-total': return fetchFederationInstancesChart(true);
|
||||
case 'users': return fetchUsersChart(false);
|
||||
case 'users-total': return fetchUsersChart(true);
|
||||
case 'active-users': return fetchActiveUsersChart();
|
||||
case 'notes': return fetchNotesChart('combined');
|
||||
case 'local-notes': return fetchNotesChart('local');
|
||||
case 'remote-notes': return fetchNotesChart('remote');
|
||||
case 'notes-total': return fetchNotesTotalChart();
|
||||
case 'drive': return fetchDriveChart();
|
||||
case 'drive-total': return fetchDriveTotalChart();
|
||||
case 'drive-files': return fetchDriveFilesChart();
|
||||
case 'drive-files-total': return fetchDriveFilesTotalChart();
|
||||
|
||||
case 'instances-requests': return fetchInstanceRequestsChart();
|
||||
case 'instances-users': return fetchInstanceUsersChart(false);
|
||||
case 'instances-users-total': return fetchInstanceUsersChart(true);
|
||||
case 'instances-notes': return fetchInstanceNotesChart(false);
|
||||
case 'instances-notes-total': return fetchInstanceNotesChart(true);
|
||||
case 'instances-ff': return fetchInstanceFfChart(false);
|
||||
case 'instances-ff-total': return fetchInstanceFfChart(true);
|
||||
case 'instances-drive-usage': return fetchInstanceDriveUsageChart(false);
|
||||
case 'instances-drive-usage-total': return fetchInstanceDriveUsageChart(true);
|
||||
case 'instances-drive-files': return fetchInstanceDriveFilesChart(false);
|
||||
case 'instances-drive-files-total': return fetchInstanceDriveFilesChart(true);
|
||||
}
|
||||
};
|
||||
fetching.value = true;
|
||||
data = await fetchData();
|
||||
fetching.value = false;
|
||||
render();
|
||||
};
|
||||
|
||||
watch(() => [props.src, props.span], fetchAndRender);
|
||||
|
||||
onMounted(() => {
|
||||
fetchAndRender();
|
||||
});
|
||||
|
||||
return {
|
||||
chartEl,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import MkButton from '../ui/button.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="vblkjoeq">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div class="input" :class="{ inline, disabled, focused }">
|
||||
<div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
|
||||
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
|
||||
<select ref="inputEl"
|
||||
<select class="select" ref="inputEl"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
|
@ -25,7 +25,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import MkButton from '../ui/button.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -81,6 +82,7 @@ export default defineComponent({
|
|||
const inputEl = ref(null);
|
||||
const prefixEl = ref(null);
|
||||
const suffixEl = ref(null);
|
||||
const container = ref(null);
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
|
@ -132,6 +134,47 @@ export default defineComponent({
|
|||
});
|
||||
});
|
||||
|
||||
const onClick = (ev: MouseEvent) => {
|
||||
focused.value = true;
|
||||
|
||||
const menu = [];
|
||||
let options = context.slots.default();
|
||||
|
||||
for (const optionOrOptgroup of options) {
|
||||
if (optionOrOptgroup.type === 'optgroup') {
|
||||
const optgroup = optionOrOptgroup;
|
||||
menu.push({
|
||||
type: 'label',
|
||||
text: optgroup.props.label,
|
||||
});
|
||||
for (const option of optgroup.children) {
|
||||
menu.push({
|
||||
text: option.children,
|
||||
active: v.value === option.props.value,
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const option = optionOrOptgroup;
|
||||
menu.push({
|
||||
text: option.children,
|
||||
active: v.value === option.props.value,
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
os.popupMenu(menu, container.value, {
|
||||
width: container.value.offsetWidth,
|
||||
}).then(() => {
|
||||
focused.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
v,
|
||||
focused,
|
||||
|
@ -141,8 +184,10 @@ export default defineComponent({
|
|||
inputEl,
|
||||
prefixEl,
|
||||
suffixEl,
|
||||
container,
|
||||
focus,
|
||||
onInput,
|
||||
onClick,
|
||||
updated,
|
||||
};
|
||||
},
|
||||
|
@ -174,8 +219,15 @@ export default defineComponent({
|
|||
> .input {
|
||||
$height: 42px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
> select {
|
||||
&:hover {
|
||||
> .select {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
}
|
||||
|
||||
> .select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
|
@ -195,10 +247,7 @@ export default defineComponent({
|
|||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.1s ease-out;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .prefix,
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import MkButton from '../ui/button.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
@ -27,8 +27,7 @@ export default defineComponent({
|
|||
default: false
|
||||
},
|
||||
customEmojis: {
|
||||
required: false,
|
||||
default: () => []
|
||||
required: false
|
||||
},
|
||||
isReaction: {
|
||||
type: Boolean,
|
||||
|
@ -58,10 +57,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
ce() {
|
||||
let ce = [];
|
||||
if (this.customEmojis) ce = ce.concat(this.customEmojis);
|
||||
if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
|
||||
return ce;
|
||||
return this.customEmojis || this.$instance?.emojis || [];
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -24,35 +24,26 @@
|
|||
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
<option value="day">{{ $ts.perDay }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<canvas ref="chart"></canvas>
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import MkSelect from './form/select.vue';
|
||||
import number from '@client/filters/number';
|
||||
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
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})`;
|
||||
};
|
||||
import { defineComponent, onMounted, ref, watch } from 'vue';
|
||||
import MkSelect from '@client/components/form/select.vue';
|
||||
import MkChart from '@client/components/chart.vue';
|
||||
import * as os from '@client/os';
|
||||
import { defaultStore } from '@client/store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSelect
|
||||
MkSelect,
|
||||
MkChart,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -68,463 +59,15 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
setup() {
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref('notes');
|
||||
|
||||
return {
|
||||
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',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
data(): any {
|
||||
if (this.chart == null) return null;
|
||||
switch (this.chartSrc) {
|
||||
case 'federation-instances': return this.federationInstancesChart(false);
|
||||
case 'federation-instances-total': return this.federationInstancesChart(true);
|
||||
case 'users': return this.usersChart(false);
|
||||
case 'users-total': return this.usersChart(true);
|
||||
case 'active-users': return this.activeUsersChart();
|
||||
case 'notes': return this.notesChart('combined');
|
||||
case 'local-notes': return this.notesChart('local');
|
||||
case 'remote-notes': return this.notesChart('remote');
|
||||
case 'notes-total': return this.notesTotalChart();
|
||||
case 'drive': return this.driveChart();
|
||||
case 'drive-total': return this.driveTotalChart();
|
||||
case 'drive-files': return this.driveFilesChart();
|
||||
case 'drive-files-total': return this.driveFilesTotalChart();
|
||||
}
|
||||
},
|
||||
|
||||
stats(): any[] {
|
||||
const stats =
|
||||
this.chartSpan == 'day' ? this.chart.perDay :
|
||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
||||
null;
|
||||
|
||||
return stats;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
chartSrc() {
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
chartSpan() {
|
||||
this.renderChart();
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.now = new Date();
|
||||
|
||||
this.fetchChart();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchChart() {
|
||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
||||
os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
|
||||
]), Promise.all([
|
||||
os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
|
||||
])]);
|
||||
|
||||
const chart = {
|
||||
perHour: {
|
||||
federation: perHour[0],
|
||||
users: perHour[1],
|
||||
activeUsers: perHour[2],
|
||||
notes: perHour[3],
|
||||
drive: perHour[4],
|
||||
},
|
||||
perDay: {
|
||||
federation: perDay[0],
|
||||
users: perDay[1],
|
||||
activeUsers: perDay[2],
|
||||
notes: perDay[3],
|
||||
drive: perDay[4],
|
||||
}
|
||||
};
|
||||
|
||||
this.chart = chart;
|
||||
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
renderChart() {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.destroy();
|
||||
}
|
||||
|
||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
this.chartInstance = markRaw(new Chart(this.$refs.chart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: this.data.series.map(x => ({
|
||||
label: x.name,
|
||||
data: x.data.slice().reverse(),
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: x.color,
|
||||
borderDash: x.borderDash || [],
|
||||
backgroundColor: alpha(x.color, 0.1),
|
||||
fill: x.fill == null ? true : x.fill,
|
||||
hidden: !!x.hidden
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 8
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: this.chartSpan == 'day' ? 'month' : 'day',
|
||||
},
|
||||
gridLines: {
|
||||
display: this.detailed,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: this.detailed
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'left',
|
||||
gridLines: {
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: this.detailed
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
getDate(ago: number) {
|
||||
const y = this.now.getFullYear();
|
||||
const m = this.now.getMonth();
|
||||
const d = this.now.getDate();
|
||||
const h = this.now.getHours();
|
||||
|
||||
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
||||
},
|
||||
|
||||
format(arr) {
|
||||
const now = Date.now();
|
||||
return arr.map((v, i) => ({
|
||||
x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
|
||||
y: v
|
||||
}));
|
||||
},
|
||||
|
||||
federationInstancesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Instances',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.federation.instance.total
|
||||
: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
|
||||
)
|
||||
}]
|
||||
chartSrc,
|
||||
chartSpan,
|
||||
};
|
||||
},
|
||||
|
||||
notesChart(type: string): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
|
||||
: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Renotes',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
|
||||
: this.stats.notes[type].diffs.renote
|
||||
)
|
||||
}, {
|
||||
name: 'Replies',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
|
||||
: this.stats.notes[type].diffs.reply
|
||||
)
|
||||
}, {
|
||||
name: 'Normal',
|
||||
type: 'area',
|
||||
color: '#FF4560',
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
|
||||
: this.stats.notes[type].diffs.normal
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
notesTotalChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.notes.local.total)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.notes.remote.total)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
usersChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? sum(this.stats.users.local.total, this.stats.users.remote.total)
|
||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(total
|
||||
? this.stats.users.local.total
|
||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(total
|
||||
? this.stats.users.remote.total
|
||||
: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
activeUsersChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.activeUsers.local.count)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.activeUsers.remote.count)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveChart(): any {
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#09d8e2',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(
|
||||
sum(
|
||||
this.stats.drive.local.incSize,
|
||||
negate(this.stats.drive.local.decSize),
|
||||
this.stats.drive.remote.incSize,
|
||||
negate(this.stats.drive.remote.decSize)
|
||||
)
|
||||
)
|
||||
}, {
|
||||
name: 'Local +',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: this.format(this.stats.drive.local.incSize)
|
||||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
color: '#FF4560',
|
||||
data: this.format(negate(this.stats.drive.local.decSize))
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.drive.remote.incSize)
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: this.format(negate(this.stats.drive.remote.decSize))
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveTotalChart(): any {
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.local.totalSize)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.remote.totalSize)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveFilesChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#09d8e2',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(
|
||||
sum(
|
||||
this.stats.drive.local.incCount,
|
||||
negate(this.stats.drive.local.decCount),
|
||||
this.stats.drive.remote.incCount,
|
||||
negate(this.stats.drive.remote.decCount)
|
||||
)
|
||||
)
|
||||
}, {
|
||||
name: 'Local +',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: this.format(this.stats.drive.local.incCount)
|
||||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
color: '#FF4560',
|
||||
data: this.format(negate(this.stats.drive.local.decCount))
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.drive.remote.incCount)
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: this.format(negate(this.stats.drive.remote.decCount))
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveFilesTotalChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.local.totalCount)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.remote.totalCount)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
number
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
47
src/client/components/number-diff.vue
Normal file
47
src/client/components/number-diff.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
|
||||
<slot name="before"></slot>{{ isPlus ? '+' : isMinus ? '-' : '' }}{{ number(value) }}<slot name="after"></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import number from '@client/filters/number';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const isPlus = computed(() => props.value > 0);
|
||||
const isMinus = computed(() => props.value < 0);
|
||||
const isZero = computed(() => props.value === 0);
|
||||
return {
|
||||
isPlus,
|
||||
isMinus,
|
||||
isZero,
|
||||
number,
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ceaaebcd {
|
||||
&.isPlus {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.isMinus {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
&.isZero {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="rrevdjwt" :class="{ center: align === 'center' }"
|
||||
:style="{ width: width ? width + 'px' : null }"
|
||||
ref="items"
|
||||
@contextmenu.self="e => e.preventDefault()"
|
||||
v-hotkey="keymap"
|
||||
|
@ -59,6 +60,10 @@ export default defineComponent({
|
|||
type: String,
|
||||
requried: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
emits: ['close'],
|
||||
data() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
|
||||
<MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
|
||||
<MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/>
|
||||
</MkPopup>
|
||||
</template>
|
||||
|
||||
|
@ -24,6 +24,10 @@ export default defineComponent({
|
|||
type: String,
|
||||
required: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
viaKeyboard: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
|
|
|
@ -372,12 +372,17 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
|
|||
});
|
||||
}
|
||||
|
||||
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
|
||||
export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: {
|
||||
align?: string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let dispose;
|
||||
popup(import('@client/components/ui/popup-menu.vue'), {
|
||||
items,
|
||||
src,
|
||||
width: options?.width,
|
||||
align: options?.align,
|
||||
viaKeyboard: options?.viaKeyboard
|
||||
}, {
|
||||
|
|
|
@ -65,17 +65,17 @@
|
|||
<div class="_debobigegoPanel cmhjzshl">
|
||||
<div class="selects">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<option value="requests">{{ $ts._instanceCharts.requests }}</option>
|
||||
<option value="users">{{ $ts._instanceCharts.users }}</option>
|
||||
<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="notes">{{ $ts._instanceCharts.notes }}</option>
|
||||
<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="ff">{{ $ts._instanceCharts.ff }}</option>
|
||||
<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||
<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
|
||||
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
|
||||
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
|
||||
<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
|
||||
<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||
<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
|
@ -83,7 +83,7 @@
|
|||
</MkSelect>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<canvas :ref="setChart"></canvas>
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -135,7 +135,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import MkChart from '@client/components/chart.vue';
|
||||
import FormObjectView from '@client/components/debobigego/object-view.vue';
|
||||
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
||||
import FormLink from '@client/components/debobigego/link.vue';
|
||||
|
@ -151,17 +151,6 @@ import bytes from '@client/filters/bytes';
|
|||
import * as symbols from '@client/symbols';
|
||||
import MkInstanceInfo from '@client/pages/instance/instance.vue';
|
||||
|
||||
const chartLimit = 90;
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
const alpha = hex => {
|
||||
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}, 0.1)`;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
|
@ -173,6 +162,7 @@ export default defineComponent({
|
|||
FormKeyValueView,
|
||||
FormSuspense,
|
||||
MkSelect,
|
||||
MkChart,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -199,53 +189,11 @@ export default defineComponent({
|
|||
dnsPromiseFactory: () => os.api('federation/dns', {
|
||||
host: this.host
|
||||
}),
|
||||
now: null,
|
||||
canvas: null,
|
||||
chart: null,
|
||||
chartInstance: null,
|
||||
chartSrc: 'requests',
|
||||
chartSrc: 'instance-requests',
|
||||
chartSpan: 'hour',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
data(): any {
|
||||
if (this.chart == null) return null;
|
||||
switch (this.chartSrc) {
|
||||
case 'requests': return this.requestsChart();
|
||||
case 'users': return this.usersChart(false);
|
||||
case 'users-total': return this.usersChart(true);
|
||||
case 'notes': return this.notesChart(false);
|
||||
case 'notes-total': return this.notesChart(true);
|
||||
case 'ff': return this.ffChart(false);
|
||||
case 'ff-total': return this.ffChart(true);
|
||||
case 'drive-usage': return this.driveUsageChart(false);
|
||||
case 'drive-usage-total': return this.driveUsageChart(true);
|
||||
case 'drive-files': return this.driveFilesChart(false);
|
||||
case 'drive-files-total': return this.driveFilesChart(true);
|
||||
}
|
||||
},
|
||||
|
||||
stats(): any[] {
|
||||
const stats =
|
||||
this.chartSpan == 'day' ? this.chart.perDay :
|
||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
||||
null;
|
||||
|
||||
return stats;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
chartSrc() {
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
chartSpan() {
|
||||
this.renderChart();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetch();
|
||||
},
|
||||
|
@ -258,190 +206,6 @@ export default defineComponent({
|
|||
this.instance = await os.api('federation/show-instance', {
|
||||
host: this.host
|
||||
});
|
||||
|
||||
this.now = new Date();
|
||||
|
||||
const [perHour, perDay] = await Promise.all([
|
||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
|
||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
|
||||
]);
|
||||
|
||||
const chart = {
|
||||
perHour: perHour,
|
||||
perDay: perDay
|
||||
};
|
||||
|
||||
this.chart = chart;
|
||||
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
setChart(el) {
|
||||
this.canvas = el;
|
||||
},
|
||||
|
||||
renderChart() {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.destroy();
|
||||
}
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
this.chartInstance = new Chart(this.canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: this.data.series.map(x => ({
|
||||
label: x.name,
|
||||
data: x.data.slice().reverse(),
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: x.color,
|
||||
backgroundColor: alpha(x.color),
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 16
|
||||
}
|
||||
},
|
||||
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',
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getDate(ago: number) {
|
||||
const y = this.now.getFullYear();
|
||||
const m = this.now.getMonth();
|
||||
const d = this.now.getDate();
|
||||
const h = this.now.getHours();
|
||||
|
||||
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
||||
},
|
||||
|
||||
format(arr) {
|
||||
return arr;
|
||||
},
|
||||
|
||||
requestsChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'In',
|
||||
color: '#008FFB',
|
||||
data: this.format(this.stats.requests.received)
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.requests.succeeded)
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
color: '#FEB019',
|
||||
data: this.format(this.stats.requests.failed)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
usersChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Users',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.users.total
|
||||
: sum(this.stats.users.inc, negate(this.stats.users.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
notesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Notes',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.notes.total
|
||||
: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
ffChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Following',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.following.total
|
||||
: sum(this.stats.following.inc, negate(this.stats.following.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Followers',
|
||||
color: '#00E396',
|
||||
data: this.format(total
|
||||
? this.stats.followers.total
|
||||
: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveUsageChart(total: boolean): any {
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Drive usage',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.drive.totalUsage
|
||||
: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveFilesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Drive files',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.drive.totalFiles
|
||||
: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
info() {
|
||||
|
|
|
@ -78,17 +78,17 @@
|
|||
<span class="label">{{ $ts.charts }}</span>
|
||||
<div class="selects">
|
||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<option value="requests">{{ $ts._instanceCharts.requests }}</option>
|
||||
<option value="users">{{ $ts._instanceCharts.users }}</option>
|
||||
<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="notes">{{ $ts._instanceCharts.notes }}</option>
|
||||
<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="ff">{{ $ts._instanceCharts.ff }}</option>
|
||||
<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||
<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
|
||||
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
|
||||
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||
<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
|
||||
<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||
<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
|
||||
<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||
<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||
<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||
<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||
<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
|
@ -97,7 +97,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<canvas :ref="setChart"></canvas>
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operations section">
|
||||
|
@ -124,28 +124,17 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||
import MkUsersDialog from '@client/components/users-dialog.vue';
|
||||
import MkSelect from '@client/components/form/select.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkSwitch from '@client/components/form/switch.vue';
|
||||
import MkInfo from '@client/components/ui/info.vue';
|
||||
import MkChart from '@client/components/chart.vue';
|
||||
import bytes from '@client/filters/bytes';
|
||||
import number from '@client/filters/number';
|
||||
import * as os from '@client/os';
|
||||
|
||||
const chartLimit = 90;
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
const alpha = hex => {
|
||||
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}, 0.1)`;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XModalWindow,
|
||||
|
@ -153,6 +142,7 @@ export default defineComponent({
|
|||
MkButton,
|
||||
MkSwitch,
|
||||
MkInfo,
|
||||
MkChart,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -167,42 +157,12 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
isSuspended: this.instance.isSuspended,
|
||||
now: null,
|
||||
canvas: null,
|
||||
chart: null,
|
||||
chartInstance: null,
|
||||
chartSrc: 'requests',
|
||||
chartSpan: 'hour',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
data(): any {
|
||||
if (this.chart == null) return null;
|
||||
switch (this.chartSrc) {
|
||||
case 'requests': return this.requestsChart();
|
||||
case 'users': return this.usersChart(false);
|
||||
case 'users-total': return this.usersChart(true);
|
||||
case 'notes': return this.notesChart(false);
|
||||
case 'notes-total': return this.notesChart(true);
|
||||
case 'ff': return this.ffChart(false);
|
||||
case 'ff-total': return this.ffChart(true);
|
||||
case 'drive-usage': return this.driveUsageChart(false);
|
||||
case 'drive-usage-total': return this.driveUsageChart(true);
|
||||
case 'drive-files': return this.driveFilesChart(false);
|
||||
case 'drive-files-total': return this.driveFilesChart(true);
|
||||
}
|
||||
},
|
||||
|
||||
stats(): any[] {
|
||||
const stats =
|
||||
this.chartSpan == 'day' ? this.chart.perDay :
|
||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
||||
null;
|
||||
|
||||
return stats;
|
||||
},
|
||||
|
||||
meta() {
|
||||
return this.$instance;
|
||||
},
|
||||
|
@ -219,49 +179,15 @@ export default defineComponent({
|
|||
isSuspended: this.isSuspended
|
||||
});
|
||||
},
|
||||
|
||||
chartSrc() {
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
chartSpan() {
|
||||
this.renderChart();
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.now = new Date();
|
||||
|
||||
const [perHour, perDay] = await Promise.all([
|
||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
|
||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
|
||||
]);
|
||||
|
||||
const chart = {
|
||||
perHour: perHour,
|
||||
perDay: perDay
|
||||
};
|
||||
|
||||
this.chart = chart;
|
||||
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
methods: {
|
||||
setChart(el) {
|
||||
this.canvas = el;
|
||||
},
|
||||
|
||||
changeBlock(e) {
|
||||
os.api('admin/update-meta', {
|
||||
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
|
||||
});
|
||||
},
|
||||
|
||||
setSrc(src) {
|
||||
this.chartSrc = src;
|
||||
},
|
||||
|
||||
removeAllFollowing() {
|
||||
os.apiWithDialog('admin/federation/remove-all-following', {
|
||||
host: this.instance.host
|
||||
|
@ -274,170 +200,6 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
|
||||
renderChart() {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.destroy();
|
||||
}
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
this.chartInstance = markRaw(new Chart(this.canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: this.data.series.map(x => ({
|
||||
label: x.name,
|
||||
data: x.data.slice().reverse(),
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: x.color,
|
||||
backgroundColor: alpha(x.color),
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
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',
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
getDate(ago: number) {
|
||||
const y = this.now.getFullYear();
|
||||
const m = this.now.getMonth();
|
||||
const d = this.now.getDate();
|
||||
const h = this.now.getHours();
|
||||
|
||||
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
||||
},
|
||||
|
||||
format(arr) {
|
||||
return arr;
|
||||
},
|
||||
|
||||
requestsChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'In',
|
||||
color: '#008FFB',
|
||||
data: this.format(this.stats.requests.received)
|
||||
}, {
|
||||
name: 'Out (succ)',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.requests.succeeded)
|
||||
}, {
|
||||
name: 'Out (fail)',
|
||||
color: '#FEB019',
|
||||
data: this.format(this.stats.requests.failed)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
usersChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Users',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.users.total
|
||||
: sum(this.stats.users.inc, negate(this.stats.users.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
notesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Notes',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.notes.total
|
||||
: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
ffChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Following',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.following.total
|
||||
: sum(this.stats.following.inc, negate(this.stats.following.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Followers',
|
||||
color: '#00E396',
|
||||
data: this.format(total
|
||||
? this.stats.followers.total
|
||||
: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveUsageChart(total: boolean): any {
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Drive usage',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.drive.totalUsage
|
||||
: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveFilesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Drive files',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.drive.totalFiles
|
||||
: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
showFollowing() {
|
||||
os.modal(MkUsersDialog, {
|
||||
title: this.$ts.instanceFollowing,
|
||||
|
|
|
@ -52,7 +52,21 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import {
|
||||
Chart,
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle
|
||||
} from 'chart.js';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkSelect from '@client/components/form/select.vue';
|
||||
import MkInput from '@client/components/form/input.vue';
|
||||
|
@ -64,6 +78,21 @@ import bytes from '@client/filters/bytes';
|
|||
import number from '@client/filters/number';
|
||||
import MkInstanceInfo from './instance.vue';
|
||||
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
PointElement,
|
||||
BarController,
|
||||
LineController,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
SubTitle
|
||||
);
|
||||
|
||||
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);
|
||||
|
@ -116,7 +145,7 @@ export default defineComponent({
|
|||
mounted() {
|
||||
this.fetchJobs();
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
os.api('admin/server-info', {}).then(res => {
|
||||
this.serverInfo = res;
|
||||
|
@ -157,7 +186,7 @@ export default defineComponent({
|
|||
datasets: [{
|
||||
label: 'CPU',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#86b300',
|
||||
backgroundColor: alpha('#86b300', 0.1),
|
||||
|
@ -165,7 +194,7 @@ export default defineComponent({
|
|||
}, {
|
||||
label: 'MEM (active)',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#935dbf',
|
||||
backgroundColor: alpha('#935dbf', 0.02),
|
||||
|
@ -173,7 +202,7 @@ export default defineComponent({
|
|||
}, {
|
||||
label: 'MEM (used)',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#935dbf',
|
||||
borderDash: [5, 5],
|
||||
|
@ -198,7 +227,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
x: {
|
||||
gridLines: {
|
||||
display: false,
|
||||
color: this.gridColor,
|
||||
|
@ -207,8 +236,8 @@ export default defineComponent({
|
|||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
},
|
||||
y: {
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
|
@ -219,7 +248,7 @@ export default defineComponent({
|
|||
display: false,
|
||||
max: 100
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
|
@ -238,7 +267,7 @@ export default defineComponent({
|
|||
datasets: [{
|
||||
label: 'In',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#94a029',
|
||||
backgroundColor: alpha('#94a029', 0.1),
|
||||
|
@ -246,7 +275,7 @@ export default defineComponent({
|
|||
}, {
|
||||
label: 'Out',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ff9156',
|
||||
backgroundColor: alpha('#ff9156', 0.1),
|
||||
|
@ -270,7 +299,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
x: {
|
||||
gridLines: {
|
||||
display: false,
|
||||
color: this.gridColor,
|
||||
|
@ -279,8 +308,8 @@ export default defineComponent({
|
|||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
},
|
||||
y: {
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
|
@ -290,7 +319,7 @@ export default defineComponent({
|
|||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
|
@ -309,7 +338,7 @@ export default defineComponent({
|
|||
datasets: [{
|
||||
label: 'Read',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#94a029',
|
||||
backgroundColor: alpha('#94a029', 0.1),
|
||||
|
@ -317,7 +346,7 @@ export default defineComponent({
|
|||
}, {
|
||||
label: 'Write',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ff9156',
|
||||
backgroundColor: alpha('#ff9156', 0.1),
|
||||
|
@ -341,7 +370,7 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
x: {
|
||||
gridLines: {
|
||||
display: false,
|
||||
color: this.gridColor,
|
||||
|
@ -350,8 +379,8 @@ export default defineComponent({
|
|||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
},
|
||||
y: {
|
||||
position: 'right',
|
||||
gridLines: {
|
||||
display: true,
|
||||
|
@ -361,7 +390,7 @@ export default defineComponent({
|
|||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
|
@ -371,18 +400,6 @@ export default defineComponent({
|
|||
}));
|
||||
},
|
||||
|
||||
async showInstanceInfo(q) {
|
||||
let instance = q;
|
||||
if (typeof q === 'string') {
|
||||
instance = await os.api('federation/show-instance', {
|
||||
host: q
|
||||
});
|
||||
}
|
||||
os.popup(MkInstanceInfo, {
|
||||
instance: instance
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
fetchJobs() {
|
||||
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
|
||||
this.jobs = jobs;
|
||||
|
|
|
@ -1,61 +1,67 @@
|
|||
<template>
|
||||
<FormBase>
|
||||
<FormSuspense :p="init">
|
||||
<FormSuspense :p="fetchStats" v-slot="{ result: stats }">
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>Users</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>Notes</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
</FormSuspense>
|
||||
<div>
|
||||
<MkHeader :info="header"/>
|
||||
|
||||
<div class="_debobigegoItem">
|
||||
<div class="_debobigegoPanel">
|
||||
<MkInstanceStats :chart-limit="300" :detailed="true"/>
|
||||
<div class="edbbcaef">
|
||||
<div class="numbers" v-if="stats">
|
||||
<div class="number _panel">
|
||||
<div class="label">Users</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(stats.originalUsersCount) }}
|
||||
<MkNumberDiff v-if="usersComparedToThePrevDay" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
</div>
|
||||
</div>
|
||||
<div class="number _panel">
|
||||
<div class="label">Notes</div>
|
||||
<div class="value _monospace">
|
||||
{{ number(stats.originalNotesCount) }}
|
||||
<MkNumberDiff v-if="notesComparedToThePrevDay" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<XMetrics/>
|
||||
<MkContainer :foldable="true" class="charts">
|
||||
<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
|
||||
<div style="padding-top: 12px;">
|
||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>Node.js</template>
|
||||
<template #value>{{ serverInfo.node }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>PostgreSQL</template>
|
||||
<template #value>{{ serverInfo.psql }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>Redis</template>
|
||||
<template #value>{{ serverInfo.redis }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
</FormSuspense>
|
||||
</FormSuspense>
|
||||
</FormBase>
|
||||
<!--<XMetrics/>-->
|
||||
|
||||
<div class="numbers">
|
||||
<div class="number _panel">
|
||||
<div class="label">Misskey</div>
|
||||
<div class="value _monospace">{{ version }}</div>
|
||||
</div>
|
||||
<div class="number _panel" v-if="serverInfo">
|
||||
<div class="label">Node.js</div>
|
||||
<div class="value _monospace">{{ serverInfo.node }}</div>
|
||||
</div>
|
||||
<div class="number _panel" v-if="serverInfo">
|
||||
<div class="label">PostgreSQL</div>
|
||||
<div class="value _monospace">{{ serverInfo.psql }}</div>
|
||||
</div>
|
||||
<div class="number _panel" v-if="serverInfo">
|
||||
<div class="label">Redis</div>
|
||||
<div class="value _monospace">{{ serverInfo.redis }}</div>
|
||||
</div>
|
||||
<div class="number _panel">
|
||||
<div class="label">Vue</div>
|
||||
<div class="value _monospace">{{ vueVersion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, markRaw } from 'vue';
|
||||
import { computed, defineComponent, version as vueVersion } from 'vue';
|
||||
import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
|
||||
import FormInput from '@client/components/debobigego/input.vue';
|
||||
import FormButton from '@client/components/debobigego/button.vue';
|
||||
import FormBase from '@client/components/debobigego/base.vue';
|
||||
import FormGroup from '@client/components/debobigego/group.vue';
|
||||
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
||||
import FormInfo from '@client/components/debobigego/info.vue';
|
||||
import FormSuspense from '@client/components/debobigego/suspense.vue';
|
||||
import MkInstanceStats from '@client/components/instance-stats.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkSelect from '@client/components/form/select.vue';
|
||||
import MkInput from '@client/components/form/input.vue';
|
||||
import MkNumberDiff from '@client/components/number-diff.vue';
|
||||
import MkContainer from '@client/components/ui/container.vue';
|
||||
import MkFolder from '@client/components/ui/folder.vue';
|
||||
import { version, url } from '@client/config';
|
||||
|
@ -68,12 +74,10 @@ import * as symbols from '@client/symbols';
|
|||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSuspense,
|
||||
FormGroup,
|
||||
FormInfo,
|
||||
MkNumberDiff,
|
||||
FormKeyValueView,
|
||||
MkInstanceStats,
|
||||
MkContainer,
|
||||
XMetrics,
|
||||
},
|
||||
|
||||
|
@ -82,17 +86,22 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.overview,
|
||||
title: this.$ts.dashboard,
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
page: 'index',
|
||||
header: {
|
||||
title: this.$ts.dashboard,
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
},
|
||||
version,
|
||||
vueVersion,
|
||||
url,
|
||||
stats: null,
|
||||
meta: null,
|
||||
fetchStats: () => os.api('stats', {}),
|
||||
fetchServerInfo: () => os.api('admin/server-info', {}),
|
||||
serverInfo: null,
|
||||
usersComparedToThePrevDay: null,
|
||||
notesComparedToThePrevDay: null,
|
||||
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
|
||||
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
|
||||
}
|
||||
|
@ -100,13 +109,29 @@ export default defineComponent({
|
|||
|
||||
async mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
|
||||
os.api('meta', { detail: true }).then(meta => {
|
||||
this.meta = meta;
|
||||
});
|
||||
|
||||
os.api('stats', {}).then(stats => {
|
||||
this.stats = stats;
|
||||
|
||||
os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
|
||||
this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
|
||||
});
|
||||
|
||||
os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
|
||||
this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
|
||||
});
|
||||
});
|
||||
|
||||
os.api('admin/server-info', {}).then(serverInfo => {
|
||||
this.serverInfo = serverInfo;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
this.meta = await os.api('meta', { detail: true });
|
||||
},
|
||||
|
||||
async showInstanceInfo(q) {
|
||||
let instance = q;
|
||||
if (typeof q === 'string') {
|
||||
|
@ -125,3 +150,36 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edbbcaef {
|
||||
> .numbers {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill,minmax(130px,1fr));
|
||||
margin: 16px;
|
||||
|
||||
> .number {
|
||||
padding: 12px 16px;
|
||||
|
||||
> .label {
|
||||
opacity: 0.7;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
> .value {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
|
||||
> .diff {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .charts {
|
||||
margin: var(--margin);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -67,7 +67,7 @@ export default defineComponent({
|
|||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
this.chart = markRaw(new Chart(this.$refs.chart, {
|
||||
type: 'line',
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as tinycolor from 'tinycolor2';
|
||||
import Chart from 'chart.js';
|
||||
import { Hpml } from './evaluator';
|
||||
import { values, utils } from '@syuilo/aiscript';
|
||||
import { Fn, HpmlScope } from '.';
|
||||
import { Expr } from './expr';
|
||||
import * as seedrandom from 'seedrandom';
|
||||
|
||||
/*
|
||||
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
|
||||
Chart.pluginService.register({
|
||||
beforeDraw: (chart, easing) => {
|
||||
|
@ -18,6 +18,7 @@ Chart.pluginService.register({
|
|||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
export function initAiLib(hpml: Hpml) {
|
||||
return {
|
||||
|
@ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) {
|
|||
]));
|
||||
}),
|
||||
'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
|
||||
/* TODO
|
||||
utils.assertString(id);
|
||||
utils.assertObject(opts);
|
||||
const canvas = hpml.canvases[id.value];
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
|
||||
Chart.defaults.global.defaultFontColor = '#555';
|
||||
Chart.defaults.color = '#555';
|
||||
const chart = new Chart(canvas, {
|
||||
type: opts.value.get('type').value,
|
||||
data: {
|
||||
|
@ -122,6 +124,7 @@ export function initAiLib(hpml: Hpml) {
|
|||
})
|
||||
}
|
||||
});
|
||||
*/
|
||||
})
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export const builtinThemes = [
|
|||
require('@client/themes/d-astro.json5'),
|
||||
require('@client/themes/d-future.json5'),
|
||||
require('@client/themes/d-botanical.json5'),
|
||||
require('@client/themes/d-pumpkin.json5'),
|
||||
require('@client/themes/d-black.json5'),
|
||||
] as Theme[];
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
|
||||
base: 'dark',
|
||||
name: 'Mi Astro',
|
||||
name: 'Mi Astro Dark',
|
||||
author: 'syuilo',
|
||||
props: {
|
||||
bg: '#232125',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
|
||||
|
||||
name: 'Mi Future',
|
||||
name: 'Mi Future Dark',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'dark',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
|
||||
|
||||
name: 'Mi Persimmon',
|
||||
name: 'Mi Persimmon Dark',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'dark',
|
||||
|
|
88
src/client/themes/d-pumpkin.json5
Normal file
88
src/client/themes/d-pumpkin.json5
Normal file
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301',
|
||||
|
||||
name: 'Mi Pumpkin Dark',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'dark',
|
||||
|
||||
props: {
|
||||
X2: ':darken<2<@panel',
|
||||
X3: 'rgba(255, 255, 255, 0.05)',
|
||||
X4: 'rgba(255, 255, 255, 0.1)',
|
||||
X5: 'rgba(255, 255, 255, 0.05)',
|
||||
X6: 'rgba(255, 255, 255, 0.15)',
|
||||
X7: 'rgba(255, 255, 255, 0.05)',
|
||||
X8: ':lighten<5<@accent',
|
||||
X9: ':darken<5<@accent',
|
||||
bg: 'rgb(37, 32, 47)',
|
||||
fg: '#e0d5c0',
|
||||
X10: ':alpha<0.4<@accent',
|
||||
X11: 'rgba(0, 0, 0, 0.3)',
|
||||
X12: 'rgba(255, 255, 255, 0.1)',
|
||||
X13: 'rgba(255, 255, 255, 0.15)',
|
||||
X14: ':alpha<0.5<@navBg',
|
||||
X15: ':alpha<0<@panel',
|
||||
X16: ':alpha<0.7<@panel',
|
||||
X17: ':alpha<0.8<@bg',
|
||||
cwBg: '#687390',
|
||||
cwFg: '#393f4f',
|
||||
link: 'rgb(172, 193, 68)',
|
||||
warn: '#ecb637',
|
||||
badge: '#31b1ce',
|
||||
error: '#ec4137',
|
||||
focus: ':alpha<0.3<@accent',
|
||||
navBg: '@panel',
|
||||
navFg: '@fg',
|
||||
panel: ':lighten<3<@bg',
|
||||
popup: ':lighten<3<@panel',
|
||||
accent: 'rgb(242, 133, 36)',
|
||||
header: ':alpha<0.7<@panel',
|
||||
infoBg: '#253142',
|
||||
infoFg: '#fff',
|
||||
renote: 'rgb(110, 179, 72)',
|
||||
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||
divider: 'rgba(255, 255, 255, 0.1)',
|
||||
hashtag: 'rgb(188, 90, 255)',
|
||||
mention: 'rgb(72, 179, 139)',
|
||||
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||
success: '#86b300',
|
||||
buttonBg: 'rgba(255, 255, 255, 0.05)',
|
||||
switchBg: 'rgba(255, 255, 255, 0.15)',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
cwHoverBg: '#707b97',
|
||||
indicator: '@accent',
|
||||
mentionMe: '@accent',
|
||||
messageBg: '@bg',
|
||||
navActive: '@accent',
|
||||
accentedBg: ':alpha<0.15<@accent',
|
||||
fgOnAccent: '#000',
|
||||
infoWarnBg: '#42321c',
|
||||
infoWarnFg: '#ffbd3e',
|
||||
navHoverFg: ':lighten<17<@fg',
|
||||
dateLabelFg: '@fg',
|
||||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
panelBorder: '" solid 1px var(--divider)',
|
||||
accentDarken: ':darken<10<@accent',
|
||||
acrylicPanel: ':alpha<0.5<@panel',
|
||||
navIndicator: '@indicator',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
fgTransparent: ':alpha<0.5<@fg',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
panelHeaderFg: '@fg',
|
||||
buttonGradateA: '@accent',
|
||||
buttonGradateB: ':hue<20<@accent',
|
||||
htmlThemeColor: '@bg',
|
||||
panelHighlight: ':lighten<3<@panel',
|
||||
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||
},
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
|
||||
|
||||
name: 'Mi Apricot',
|
||||
name: 'Mi Apricot Light',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'light',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
|
||||
|
||||
name: 'Mi Rainy',
|
||||
name: 'Mi Rainy Light',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'light',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
id: '6128c2a9-5c54-43fe-a47d-17942356470b',
|
||||
|
||||
name: 'Mi Vivid',
|
||||
name: 'Mi Vivid Light',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'light',
|
||||
|
|
|
@ -10,7 +10,7 @@ import procesObjectStorage from './processors/object-storage/index';
|
|||
import { queueLogger } from './logger';
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
import { getJobInfo } from './get-job-info';
|
||||
import { dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
|
||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
|
||||
import { ThinUser } from './types';
|
||||
import { IActivity } from '@/remote/activitypub/type';
|
||||
|
||||
|
@ -22,11 +22,20 @@ function renderError(e: Error): any {
|
|||
};
|
||||
}
|
||||
|
||||
const systemLogger = queueLogger.createSubLogger('system');
|
||||
const deliverLogger = queueLogger.createSubLogger('deliver');
|
||||
const inboxLogger = queueLogger.createSubLogger('inbox');
|
||||
const dbLogger = queueLogger.createSubLogger('db');
|
||||
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
|
||||
|
||||
systemQueue
|
||||
.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
||||
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
|
||||
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||
.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
deliverQueue
|
||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
|
@ -220,12 +229,17 @@ export function createCleanRemoteFilesJob() {
|
|||
}
|
||||
|
||||
export default function() {
|
||||
if (!envOption.onlyServer) {
|
||||
if (envOption.onlyServer) return;
|
||||
|
||||
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
|
||||
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
|
||||
processDb(dbQueue);
|
||||
procesObjectStorage(objectStorageQueue);
|
||||
}
|
||||
|
||||
systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { cron: '0 0 * * *' }
|
||||
});
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
|
|
12
src/queue/processors/system/index.ts
Normal file
12
src/queue/processors/system/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as Bull from 'bull';
|
||||
import { resyncCharts } from './resync-charts';
|
||||
|
||||
const jobs = {
|
||||
resyncCharts,
|
||||
} as Record<string, Bull.ProcessCallbackFunction<{}> | Bull.ProcessPromiseFunction<{}>>;
|
||||
|
||||
export default function(dbQueue: Bull.Queue<{}>) {
|
||||
for (const [k, v] of Object.entries(jobs)) {
|
||||
dbQueue.process(k, v);
|
||||
}
|
||||
}
|
21
src/queue/processors/system/resync-charts.ts
Normal file
21
src/queue/processors/system/resync-charts.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as Bull from 'bull';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import { driveChart, notesChart, usersChart } from '@/services/chart/index';
|
||||
|
||||
const logger = queueLogger.createSubLogger('resync-charts');
|
||||
|
||||
export default async function resyncCharts(job: Bull.Job<{}>, done: any): Promise<void> {
|
||||
logger.info(`Resync charts...`);
|
||||
|
||||
// TODO: ユーザーごとのチャートも更新する
|
||||
// TODO: インスタンスごとのチャートも更新する
|
||||
await Promise.all([
|
||||
driveChart.resync(),
|
||||
notesChart.resync(),
|
||||
usersChart.resync(),
|
||||
]);
|
||||
|
||||
logger.succ(`All charts successfully resynced.`);
|
||||
done();
|
||||
}
|
|
@ -2,6 +2,7 @@ import config from '@/config/index';
|
|||
import { initialize as initializeQueue } from './initialize';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
|
||||
|
||||
export const systemQueue = initializeQueue<{}>('system');
|
||||
export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
|
||||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
|
||||
export const dbQueue = initializeQueue<DbJobData>('db');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import define from '../../define';
|
||||
import { driveChart, notesChart, usersChart, instanceChart } from '@/services/chart/index';
|
||||
import { driveChart, notesChart, usersChart } from '@/services/chart/index';
|
||||
import { insertModerationLog } from '@/services/insert-moderation-log';
|
||||
|
||||
export const meta = {
|
||||
|
@ -15,7 +15,7 @@ export default define(meta, async (ps, me) => {
|
|||
driveChart.resync();
|
||||
notesChart.resync();
|
||||
usersChart.resync();
|
||||
instanceChart.resync();
|
||||
|
||||
// TODO: ユーザーごとのチャートもキューに入れて更新する
|
||||
// TODO: インスタンスごとのチャートもキューに入れて更新する
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"short_name": "Misskey",
|
||||
"name": "Misskey",
|
||||
"start_url": "/",
|
||||
"display": "minimal-ui",
|
||||
"display": "standalone",
|
||||
"background_color": "#313a42",
|
||||
"theme_color": "#86b300",
|
||||
"icons": [
|
||||
|
|
46
yarn.lock
46
yarn.lock
|
@ -2664,28 +2664,22 @@ character-parser@^2.2.0:
|
|||
dependencies:
|
||||
is-regex "^1.0.3"
|
||||
|
||||
chart.js@2.9.4:
|
||||
version "2.9.4"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
|
||||
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
|
||||
dependencies:
|
||||
chartjs-color "^2.1.0"
|
||||
moment "^2.10.2"
|
||||
chart.js@3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.1.tgz#73e24d23a4134a70ccdb5e79a917f156b6f3644a"
|
||||
integrity sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==
|
||||
|
||||
chartjs-color-string@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
|
||||
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
chartjs-adapter-date-fns@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
|
||||
integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
|
||||
|
||||
chartjs-color@^2.1.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
|
||||
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
|
||||
chartjs-plugin-zoom@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.1.1.tgz#8a28923a17fcb5eb57a0dc94c5113bf402677647"
|
||||
integrity sha512-1q54WOzK7FtAjkbemQeqvmFUV0btNYIQny2HbQ6Awq9wUtCz7Zmj6vIgp3C1DYMQwN0nqgpC3vnApqiwI7cSdQ==
|
||||
dependencies:
|
||||
chartjs-color-string "^0.6.0"
|
||||
color-convert "^1.9.3"
|
||||
hammerjs "^2.0.8"
|
||||
|
||||
check-more-types@2.24.0, check-more-types@^2.24.0:
|
||||
version "2.24.0"
|
||||
|
@ -2974,7 +2968,7 @@ collection-visit@^1.0.0:
|
|||
map-visit "^1.0.0"
|
||||
object-visit "^1.0.0"
|
||||
|
||||
color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.3:
|
||||
color-convert@^1.3.0, color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
|
@ -3616,6 +3610,11 @@ data-urls@^2.0.0:
|
|||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
date-fns@2.25.0:
|
||||
version "2.25.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
|
||||
integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
|
||||
|
||||
date-fns@^2.16.1:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
|
||||
|
@ -5300,6 +5299,11 @@ gulplog@^1.0.0:
|
|||
dependencies:
|
||||
glogg "^1.0.0"
|
||||
|
||||
hammerjs@^2.0.8:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
|
||||
integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
|
@ -7383,7 +7387,7 @@ moment-timezone@^0.5.25:
|
|||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2:
|
||||
"moment@>= 2.9.0", moment@^2.22.2:
|
||||
version "2.24.0"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
||||
|
|
Loading…
Reference in a new issue