diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd675deae..dcb67470b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 0aae1b3329..ce0aa09417 100644 --- a/README.md +++ b/README.md @@ -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 ---------------------------------------------------------------- diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 87b100772f..0000000000 --- a/docs/README.md +++ /dev/null @@ -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) diff --git a/docs/docker.en.md b/docs/docker.en.md deleted file mode 100644 index adeafe3d31..0000000000 --- a/docs/docker.en.md +++ /dev/null @@ -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! diff --git a/docs/docker.fr.md b/docs/docker.fr.md deleted file mode 100644 index 840e5b5a28..0000000000 --- a/docs/docker.fr.md +++ /dev/null @@ -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 ! diff --git a/docs/docker.ja.md b/docs/docker.ja.md deleted file mode 100644 index c660a9041b..0000000000 --- a/docs/docker.ja.md +++ /dev/null @@ -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` - ----------------------------------------------------------------- - -なにかお困りのことがありましたらお気軽にご連絡ください。 diff --git a/docs/docker.zh.md b/docs/docker.zh.md deleted file mode 100644 index 5a494ea11e..0000000000 --- a/docs/docker.zh.md +++ /dev/null @@ -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` - ----------------------------------------------------------------- - -如果您有任何疑问或是困惑,欢迎与我们联系! diff --git a/docs/examples/misskey.nginx b/docs/examples/misskey.nginx deleted file mode 100644 index b558fb023a..0000000000 --- a/docs/examples/misskey.nginx +++ /dev/null @@ -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; - } -} diff --git a/docs/manage.en.md b/docs/manage.en.md deleted file mode 100644 index d310e9531f..0000000000 --- a/docs/manage.en.md +++ /dev/null @@ -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 -``` diff --git a/docs/manage.fr.md b/docs/manage.fr.md deleted file mode 100644 index 0b2b7ffc16..0000000000 --- a/docs/manage.fr.md +++ /dev/null @@ -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 -``` diff --git a/docs/manage.ja.md b/docs/manage.ja.md deleted file mode 100644 index 55596add10..0000000000 --- a/docs/manage.ja.md +++ /dev/null @@ -1,14 +0,0 @@ -# 運営ガイド - -## ジョブキューの状態を調べる -coming soon - -## 管理者ユーザーを設定する -``` shell -node built/tools/mark-admin (ユーザー名) -``` - -例: -``` shell -node built/tools/mark-admin @syuilo -``` diff --git a/docs/manage.zh.md b/docs/manage.zh.md deleted file mode 100644 index 520d150203..0000000000 --- a/docs/manage.zh.md +++ /dev/null @@ -1,14 +0,0 @@ -# 运营指南 - -## 检查任务队列的状态 -即将到来…… - -## 设置用户为管理员 -``` shell -node built/tools/mark-admin (用户名) -``` - -样例 -``` shell -node built/tools/mark-admin @syuilo -``` diff --git a/docs/push-docker-hub.ja.md b/docs/push-docker-hub.ja.md deleted file mode 100644 index 923e4c16e0..0000000000 --- a/docs/push-docker-hub.ja.md +++ /dev/null @@ -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を選択してワークフローを実行します。 -ただし、この場合作成されるタグは`<ブランチ名>`になります。 diff --git a/docs/setup.en.md b/docs/setup.en.md deleted file mode 100644 index dfe50a6600..0000000000 --- a/docs/setup.en.md +++ /dev/null @@ -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! diff --git a/docs/setup.fr.md b/docs/setup.fr.md deleted file mode 100644 index f38c7a8eab..0000000000 --- a/docs/setup.fr.md +++ /dev/null @@ -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 ! diff --git a/docs/setup.ja.md b/docs/setup.ja.md deleted file mode 100644 index 5681ee8c51..0000000000 --- a/docs/setup.ja.md +++ /dev/null @@ -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`すると直る場合があります。 - ----------------------------------------------------------------- - -なにかお困りのことがありましたらお気軽にご連絡ください。 diff --git a/docs/setup.zh.md b/docs/setup.zh.md deleted file mode 100644 index 26a72f0d05..0000000000 --- a/docs/setup.zh.md +++ /dev/null @@ -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` ) - ----------------------------------------------------------------- - -如果您有任何疑问或是困惑,欢迎与我们联系! diff --git a/package.json b/package.json index 10ddea2c1e..f60a51b940 100644 --- a/package.json +++ b/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", diff --git a/src/client/components/chart.vue b/src/client/components/chart.vue new file mode 100644 index 0000000000..3599266cb6 --- /dev/null +++ b/src/client/components/chart.vue @@ -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> diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue index d7b6f77519..591eda9ed5 100644 --- a/src/client/components/form/input.vue +++ b/src/client/components/form/input.vue @@ -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({ diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue index 257e2cc990..30ccfd312b 100644 --- a/src/client/components/form/select.vue +++ b/src/client/components/form/select.vue @@ -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, diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue index 50be69f930..048e9032df 100644 --- a/src/client/components/form/textarea.vue +++ b/src/client/components/form/textarea.vue @@ -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({ diff --git a/src/client/components/global/emoji.vue b/src/client/components/global/emoji.vue index f4ebd5f3b3..f92e35c38f 100644 --- a/src/client/components/global/emoji.vue +++ b/src/client/components/global/emoji.vue @@ -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 || []; } }, diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue index 5e7c71ea65..fd0b75609f 100644 --- a/src/client/components/instance-stats.vue +++ b/src/client/components/instance-stats.vue @@ -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', - } + chartSrc, + chartSpan, + }; }, - - 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)) - ) - }] - }; - }, - - 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> diff --git a/src/client/components/number-diff.vue b/src/client/components/number-diff.vue new file mode 100644 index 0000000000..ba7e6964de --- /dev/null +++ b/src/client/components/number-diff.vue @@ -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> diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue index da24d90170..aaef527f1a 100644 --- a/src/client/components/ui/menu.vue +++ b/src/client/components/ui/menu.vue @@ -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() { diff --git a/src/client/components/ui/popup-menu.vue b/src/client/components/ui/popup-menu.vue index 23f7c89f3b..3ff4c658b1 100644 --- a/src/client/components/ui/popup-menu.vue +++ b/src/client/components/ui/popup-menu.vue @@ -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 diff --git a/src/client/os.ts b/src/client/os.ts index 7ae774dd92..743d2d131f 100644 --- a/src/client/os.ts +++ b/src/client/os.ts @@ -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 }, { diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue index 4fbf104f0c..7a4cd5f016 100644 --- a/src/client/pages/instance-info.vue +++ b/src/client/pages/instance-info.vue @@ -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() { diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue index 6117f090de..5572fbbf75 100644 --- a/src/client/pages/instance/instance.vue +++ b/src/client/pages/instance/instance.vue @@ -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, diff --git a/src/client/pages/instance/metrics.vue b/src/client/pages/instance/metrics.vue index 1606063aee..da36f6c688 100644 --- a/src/client/pages/instance/metrics.vue +++ b/src/client/pages/instance/metrics.vue @@ -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; diff --git a/src/client/pages/instance/overview.vue b/src/client/pages/instance/overview.vue index c6db9d0c04..4a01eeb751 100644 --- a/src/client/pages/instance/overview.vue +++ b/src/client/pages/instance/overview.vue @@ -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 class="_debobigegoItem"> - <div class="_debobigegoPanel"> - <MkInstanceStats :chart-limit="300" :detailed="true"/> +<div> + <MkHeader :info="header"/> + + <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> + + <!--<XMetrics/>--> - <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> + <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> diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue index 887fe9a574..4f8fd762bb 100644 --- a/src/client/pages/instance/queue.chart.vue +++ b/src/client/pages/instance/queue.chart.vue @@ -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', diff --git a/src/client/scripts/hpml/lib.ts b/src/client/scripts/hpml/lib.ts index 150a04732f..200faf820b 100644 --- a/src/client/scripts/hpml/lib.ts +++ b/src/client/scripts/hpml/lib.ts @@ -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) { }) } }); + */ }) }; } diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts index ad1b033edf..8b63821293 100644 --- a/src/client/scripts/theme.ts +++ b/src/client/scripts/theme.ts @@ -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[]; diff --git a/src/client/themes/d-astro.json5 b/src/client/themes/d-astro.json5 index 2350e3d46d..c6a927ec3a 100644 --- a/src/client/themes/d-astro.json5 +++ b/src/client/themes/d-astro.json5 @@ -1,7 +1,7 @@ { id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea', base: 'dark', - name: 'Mi Astro', + name: 'Mi Astro Dark', author: 'syuilo', props: { bg: '#232125', diff --git a/src/client/themes/d-future.json5 b/src/client/themes/d-future.json5 index 1882609121..b6fa1ab0c1 100644 --- a/src/client/themes/d-future.json5 +++ b/src/client/themes/d-future.json5 @@ -1,7 +1,7 @@ { id: '32a637ef-b47a-4775-bb7b-bacbb823f865', - name: 'Mi Future', + name: 'Mi Future Dark', author: 'syuilo', base: 'dark', diff --git a/src/client/themes/d-persimmon.json5 b/src/client/themes/d-persimmon.json5 index 11e9994f5e..e36265ff10 100644 --- a/src/client/themes/d-persimmon.json5 +++ b/src/client/themes/d-persimmon.json5 @@ -1,7 +1,7 @@ { id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', - name: 'Mi Persimmon', + name: 'Mi Persimmon Dark', author: 'syuilo', base: 'dark', diff --git a/src/client/themes/d-pumpkin.json5 b/src/client/themes/d-pumpkin.json5 new file mode 100644 index 0000000000..064ca4577b --- /dev/null +++ b/src/client/themes/d-pumpkin.json5 @@ -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)', + }, +} diff --git a/src/client/themes/l-apricot.json5 b/src/client/themes/l-apricot.json5 index 74cb24d407..1ed5525575 100644 --- a/src/client/themes/l-apricot.json5 +++ b/src/client/themes/l-apricot.json5 @@ -1,7 +1,7 @@ { id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', - name: 'Mi Apricot', + name: 'Mi Apricot Light', author: 'syuilo', base: 'light', diff --git a/src/client/themes/l-rainy.json5 b/src/client/themes/l-rainy.json5 index 1edde1cabf..283dd74c6c 100644 --- a/src/client/themes/l-rainy.json5 +++ b/src/client/themes/l-rainy.json5 @@ -1,7 +1,7 @@ { id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', - name: 'Mi Rainy', + name: 'Mi Rainy Light', author: 'syuilo', base: 'light', diff --git a/src/client/themes/l-vivid.json5 b/src/client/themes/l-vivid.json5 index 0f4abe0a45..b3c08f38ae 100644 --- a/src/client/themes/l-vivid.json5 +++ b/src/client/themes/l-vivid.json5 @@ -1,7 +1,7 @@ { id: '6128c2a9-5c54-43fe-a47d-17942356470b', - name: 'Mi Vivid', + name: 'Mi Vivid Light', author: 'syuilo', base: 'light', diff --git a/src/queue/index.ts b/src/queue/index.ts index 43c062bae7..37eb809604 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -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) { - deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); - inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); - processDb(dbQueue); - procesObjectStorage(objectStorageQueue); - } + 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() { diff --git a/src/queue/processors/system/index.ts b/src/queue/processors/system/index.ts new file mode 100644 index 0000000000..52b7868105 --- /dev/null +++ b/src/queue/processors/system/index.ts @@ -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); + } +} diff --git a/src/queue/processors/system/resync-charts.ts b/src/queue/processors/system/resync-charts.ts new file mode 100644 index 0000000000..b36b024cfb --- /dev/null +++ b/src/queue/processors/system/resync-charts.ts @@ -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(); +} diff --git a/src/queue/queues.ts b/src/queue/queues.ts index d8c09ef86e..a66a7ca451 100644 --- a/src/queue/queues.ts +++ b/src/queue/queues.ts @@ -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'); diff --git a/src/server/api/endpoints/admin/resync-chart.ts b/src/server/api/endpoints/admin/resync-chart.ts index b0e687333f..e01dfce1b6 100644 --- a/src/server/api/endpoints/admin/resync-chart.ts +++ b/src/server/api/endpoints/admin/resync-chart.ts @@ -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: インスタンスごとのチャートもキューに入れて更新する }); diff --git a/src/server/web/manifest.json b/src/server/web/manifest.json index db97531bbf..48030a2980 100644 --- a/src/server/web/manifest.json +++ b/src/server/web/manifest.json @@ -2,7 +2,7 @@ "short_name": "Misskey", "name": "Misskey", "start_url": "/", - "display": "minimal-ui", + "display": "standalone", "background_color": "#313a42", "theme_color": "#86b300", "icons": [ diff --git a/yarn.lock b/yarn.lock index e2140e185a..449390a6a2 100644 --- a/yarn.lock +++ b/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==