Compare commits

..

2 Commits

Author SHA1 Message Date
e86bd723d1 Update Octoverse to README (#10859)
### Type of change

- [x] Documentation Update
2025-10-29 00:34:39 +08:00
2c0035dcea Feat: Admin UI (#10857)
### What problem does this PR solve?

Add admin UI for RAGFlow

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-10-28 22:25:43 +08:00
32 changed files with 4617 additions and 187 deletions

View File

@ -43,7 +43,9 @@
<a href="https://demo.ragflow.io">Demo</a>
</h4>
#
<div align="center" style="margin-top:20px;margin-bottom:20px;">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@ -175,22 +177,21 @@ releases! 🌟
> ```bash
> vm.max_map_count=262144
> ```
>
2. Clone the repo:
```bash
$ git clone https://github.com/infiniflow/ragflow.git
```
3. Start up the server using the pre-built Docker images:
> [!CAUTION]
> All Docker images are built for x86 platforms. We don't currently offer Docker images for ARM64.
> If you are on an ARM64 platform, follow [this guide](https://ragflow.io/docs/dev/build_docker_image) to build a Docker image compatible with your system.
> The command below downloads the `v0.21.1-slim` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.21.1-slim`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server.
> The command below downloads the `v0.21.1-slim` edition of the RAGFlow Docker image. See the following table for descriptions of different RAGFlow editions. To download a RAGFlow edition different from `v0.21.1-slim`, update the `RAGFLOW_IMAGE` variable accordingly in **docker/.env** before using `docker compose` to start the server.
```bash
```bash
$ cd ragflow/docker
# Use CPU for embedding and DeepDoc tasks:
$ docker compose -f docker-compose.yml up -d
@ -198,16 +199,15 @@ releases! 🌟
# To use GPU to accelerate embedding and DeepDoc tasks:
# sed -i '1i DEVICE=gpu' .env
# docker compose -f docker-compose.yml up -d
```
```
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
|-------------------|-----------------|-----------------------|--------------------------|
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;2 | ❌ | _Unstable_ nightly build |
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | -------------------------- |
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;2 | ❌ | _Unstable_ nightly build |
> Note: Starting with `v0.22.0`, we ship only the slim edition and no longer append the **-slim** suffix to the image tag.
> Note: Starting with `v0.22.0`, we ship only the slim edition and no longer append the **-slim** suffix to the image tag.
4. Check the server status after having the server up and running:
@ -230,14 +230,17 @@ releases! 🌟
> If you skip this confirmation step and directly log in to RAGFlow, your browser may prompt a `network anormal`
> error because, at that moment, your RAGFlow may not be fully initialized.
>
5. In your web browser, enter the IP address of your server and log in to RAGFlow.
> With the default settings, you only need to enter `http://IP_OF_YOUR_MACHINE` (**sans** port number) as the default
> HTTP serving port `80` can be omitted when using the default configurations.
>
6. In [service_conf.yaml.template](./docker/service_conf.yaml.template), select the desired LLM factory in `user_default_llm` and update
the `API_KEY` field with the corresponding API key.
> See [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) for more information.
>
_The show is on!_
@ -276,7 +279,6 @@ RAGFlow uses Elasticsearch by default for storing full text and vectors. To swit
> `-v` will delete the docker container volumes, and the existing data will be cleared.
2. Set `DOC_ENGINE` in **docker/.env** to `infinity`.
3. Start the containers:
```bash
@ -303,7 +305,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```bash
pipx install uv pre-commit
```
2. Clone the source code and install Python dependencies:
```bash
@ -313,7 +314,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
uv run download_deps.py
pre-commit install
```
3. Launch the dependent services (MinIO, Elasticsearch, Redis, and MySQL) using Docker Compose:
```bash
@ -325,13 +325,11 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```
127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
```
4. If you cannot access HuggingFace, set the `HF_ENDPOINT` environment variable to use a mirror site:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```
5. If your operating system does not have jemalloc, please install it as follows:
```bash
@ -344,7 +342,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
# macOS
sudo brew install jemalloc
```
6. Launch backend service:
```bash
@ -352,14 +349,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
```
7. Install frontend dependencies:
```bash
cd web
npm install
```
8. Launch frontend service:
```bash
@ -369,14 +364,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
_The following output confirms a successful launch of the system:_
![](https://github.com/user-attachments/assets/0daf462c-a24d-4496-a66f-92533534e187)
9. Stop RAGFlow front-end and back-end service after development is complete:
```bash
pkill -f "ragflow_server.py|task_executor.py"
```
## 📚 Documentation
- [Quickstart](https://ragflow.io/docs/dev/)

View File

@ -43,7 +43,13 @@
<a href="https://demo.ragflow.io">Demo</a>
</h4>
#
<div align="center" style="margin-top:20px;margin-bottom:20px;">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<details open>
<summary><b>📕 Daftar Isi </b> </summary>
@ -169,13 +175,12 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
> ```bash
> vm.max_map_count=262144
> ```
>
2. Clone repositori:
```bash
$ git clone https://github.com/infiniflow/ragflow.git
```
3. Bangun image Docker pre-built dan jalankan server:
> [!CAUTION]
@ -184,7 +189,7 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
> Perintah di bawah ini mengunduh edisi v0.21.1 dari gambar Docker RAGFlow. Silakan merujuk ke tabel berikut untuk deskripsi berbagai edisi RAGFlow. Untuk mengunduh edisi RAGFlow yang berbeda dari v0.21.1, perbarui variabel RAGFLOW_IMAGE di docker/.env sebelum menggunakan docker compose untuk memulai server.
```bash
```bash
$ cd ragflow/docker
# Use CPU for embedding and DeepDoc tasks:
$ docker compose -f docker-compose.yml up -d
@ -192,12 +197,12 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
# To use GPU to accelerate embedding and DeepDoc tasks:
# sed -i '1i DEVICE=gpu' .env
# docker compose -f docker-compose.yml up -d
```
```
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | -------------------------- |
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;2 | ❌ | _Unstable_ nightly build |
> Catatan: Mulai dari `v0.22.0`, kami hanya menyediakan edisi slim dan tidak lagi menambahkan akhiran **-slim** pada tag image.
@ -223,14 +228,17 @@ Coba demo kami di [https://demo.ragflow.io](https://demo.ragflow.io).
> Jika Anda melewatkan langkah ini dan langsung login ke RAGFlow, browser Anda mungkin menampilkan error `network anormal`
> karena RAGFlow mungkin belum sepenuhnya siap.
>
2. Buka browser web Anda, masukkan alamat IP server Anda, dan login ke RAGFlow.
> Dengan pengaturan default, Anda hanya perlu memasukkan `http://IP_DEVICE_ANDA` (**tanpa** nomor port) karena
> port HTTP default `80` bisa dihilangkan saat menggunakan konfigurasi default.
>
3. Dalam [service_conf.yaml.template](./docker/service_conf.yaml.template), pilih LLM factory yang diinginkan di `user_default_llm` dan perbarui
bidang `API_KEY` dengan kunci API yang sesuai.
> Lihat [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) untuk informasi lebih lanjut.
>
_Sistem telah siap digunakan!_
@ -269,7 +277,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```bash
pipx install uv pre-commit
```
2. Clone kode sumber dan instal dependensi Python:
```bash
@ -279,7 +286,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
uv run download_deps.py
pre-commit install
```
3. Jalankan aplikasi yang diperlukan (MinIO, Elasticsearch, Redis, dan MySQL) menggunakan Docker Compose:
```bash
@ -291,13 +297,11 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```
127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
```
4. Jika Anda tidak dapat mengakses HuggingFace, atur variabel lingkungan `HF_ENDPOINT` untuk menggunakan situs mirror:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```
5. Jika sistem operasi Anda tidak memiliki jemalloc, instal sebagai berikut:
```bash
@ -308,7 +312,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
# mac
sudo brew install jemalloc
```
6. Jalankan aplikasi backend:
```bash
@ -316,14 +319,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
```
7. Instal dependensi frontend:
```bash
cd web
npm install
```
8. Jalankan aplikasi frontend:
```bash
@ -333,15 +334,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
_Output berikut menandakan bahwa sistem berhasil diluncurkan:_
![](https://github.com/user-attachments/assets/0daf462c-a24d-4496-a66f-92533534e187)
9. Hentikan layanan front-end dan back-end RAGFlow setelah pengembangan selesai:
```bash
pkill -f "ragflow_server.py|task_executor.py"
```
## 📚 Dokumentasi
- [Quickstart](https://ragflow.io/docs/dev/)

View File

@ -43,7 +43,13 @@
<a href="https://demo.ragflow.io">Demo</a>
</h4>
#
<div align="center" style="margin-top:20px;margin-bottom:20px;">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
## 💡 RAGFlow とは?
@ -148,22 +154,21 @@
> ```bash
> vm.max_map_count=262144
> ```
>
2. リポジトリをクローンする:
```bash
$ git clone https://github.com/infiniflow/ragflow.git
```
3. ビルド済みの Docker イメージをビルドし、サーバーを起動する:
> [!CAUTION]
> 現在、公式に提供されているすべての Docker イメージは x86 アーキテクチャ向けにビルドされており、ARM64 用の Docker イメージは提供されていません。
> ARM64 アーキテクチャのオペレーティングシステムを使用している場合は、[このドキュメント](https://ragflow.io/docs/dev/build_docker_image)を参照して Docker イメージを自分でビルドしてください。
> 以下のコマンドは、RAGFlow Docker イメージの v0.21.1 エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.21.1 とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。
> 以下のコマンドは、RAGFlow Docker イメージの v0.21.1 エディションをダウンロードします。異なる RAGFlow エディションの説明については、以下の表を参照してください。v0.21.1 とは異なるエディションをダウンロードするには、docker/.env ファイルの RAGFLOW_IMAGE 変数を適宜更新し、docker compose を使用してサーバーを起動してください。
```bash
```bash
$ cd ragflow/docker
# Use CPU for embedding and DeepDoc tasks:
$ docker compose -f docker-compose.yml up -d
@ -171,15 +176,15 @@
# To use GPU to accelerate embedding and DeepDoc tasks:
# sed -i '1i DEVICE=gpu' .env
# docker compose -f docker-compose.yml up -d
```
```
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;2 | ❌ | _Unstable_ nightly build |
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | -------------------------- |
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;2 | ❌ | _Unstable_ nightly build |
> 注意:`v0.22.0` 以降、当プロジェクトでは slim エディションのみを提供し、イメージタグに **-slim** サフィックスを付けなくなりました。
> 注意:`v0.22.0` 以降、当プロジェクトでは slim エディションのみを提供し、イメージタグに **-slim** サフィックスを付けなくなりました。
1. サーバーを立ち上げた後、サーバーの状態を確認する:
@ -200,12 +205,15 @@
```
> もし確認ステップをスキップして直接 RAGFlow にログインした場合、その時点で RAGFlow が完全に初期化されていない可能性があるため、ブラウザーがネットワーク異常エラーを表示するかもしれません。
>
2. ウェブブラウザで、プロンプトに従ってサーバーの IP アドレスを入力し、RAGFlow にログインします。
> デフォルトの設定を使用する場合、デフォルトの HTTP サービングポート `80` は省略できるので、与えられたシナリオでは、`http://IP_OF_YOUR_MACHINE`(ポート番号は省略)だけを入力すればよい。
>
3. [service_conf.yaml.template](./docker/service_conf.yaml.template) で、`user_default_llm` で希望の LLM ファクトリを選択し、`API_KEY` フィールドを対応する API キーで更新する。
> 詳しくは [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) を参照してください。
>
_これで初期設定完了ショーの開幕です_
@ -234,18 +242,22 @@
RAGFlow はデフォルトで Elasticsearch を使用して全文とベクトルを保存します。Infinityに切り替えhttps://github.com/infiniflow/infinity/)、次の手順に従います。
1. 実行中のすべてのコンテナを停止するには:
```bash
$ docker compose -f docker/docker-compose.yml down -v
```
Note: `-v` は docker コンテナのボリュームを削除し、既存のデータをクリアします。
2. **docker/.env** の「DOC \_ ENGINE」を「infinity」に設定します。
3. 起動コンテナ:
```bash
$ docker compose -f docker-compose.yml up -d
```
> [!WARNING]
> Linux/arm64 マシンでの Infinity への切り替えは正式にサポートされていません。
>
## 🔧 ソースコードで Docker イメージを作成(埋め込みモデルなし)
@ -264,7 +276,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```bash
pipx install uv pre-commit
```
2. ソースコードをクローンし、Python の依存関係をインストールする:
```bash
@ -274,7 +285,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
uv run download_deps.py
pre-commit install
```
3. Docker Compose を使用して依存サービスMinIO、Elasticsearch、Redis、MySQLを起動する:
```bash
@ -286,13 +296,11 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```
127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
```
4. HuggingFace にアクセスできない場合は、`HF_ENDPOINT` 環境変数を設定してミラーサイトを使用してください:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```
5. オペレーティングシステムにjemallocがない場合は、次のようにインストールします:
```bash
@ -303,7 +311,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
# mac
sudo brew install jemalloc
```
6. バックエンドサービスを起動する:
```bash
@ -311,14 +318,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
```
7. フロントエンドの依存関係をインストールする:
```bash
cd web
npm install
```
8. フロントエンドサービスを起動する:
```bash
@ -328,14 +333,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
_以下の画面で、システムが正常に起動したことを示します:_
![](https://github.com/user-attachments/assets/0daf462c-a24d-4496-a66f-92533534e187)
9. 開発が完了したら、RAGFlow のフロントエンド サービスとバックエンド サービスを停止します:
```bash
pkill -f "ragflow_server.py|task_executor.py"
```
## 📚 ドキュメンテーション
- [Quickstart](https://ragflow.io/docs/dev/)

View File

@ -43,7 +43,14 @@
<a href="https://demo.ragflow.io">Demo</a>
</h4>
#
<div align="center" style="margin-top:20px;margin-bottom:20px;">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
## 💡 RAGFlow란?

View File

@ -43,7 +43,13 @@
<a href="https://demo.ragflow.io">Demo</a>
</h4>
#
<div align="center" style="margin-top:20px;margin-bottom:20px;">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
<details open>
<summary><b>📕 Índice</b></summary>
@ -148,42 +154,41 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
### 🚀 Iniciar o servidor
1. Certifique-se de que `vm.max_map_count` >= 262144:
1. Certifique-se de que `vm.max_map_count` >= 262144:
> Para verificar o valor de `vm.max_map_count`:
>
> ```bash
> $ sysctl vm.max_map_count
> ```
>
> Se necessário, redefina `vm.max_map_count` para um valor de pelo menos 262144:
>
> ```bash
> # Neste caso, defina para 262144:
> $ sudo sysctl -w vm.max_map_count=262144
> ```
>
> Essa mudança será resetada após a reinicialização do sistema. Para garantir que a alteração permaneça permanente, adicione ou atualize o valor de `vm.max_map_count` em **/etc/sysctl.conf**:
>
> ```bash
> vm.max_map_count=262144
> ```
> Para verificar o valor de `vm.max_map_count`:
>
> ```bash
> $ sysctl vm.max_map_count
> ```
>
> Se necessário, redefina `vm.max_map_count` para um valor de pelo menos 262144:
>
> ```bash
> # Neste caso, defina para 262144:
> $ sudo sysctl -w vm.max_map_count=262144
> ```
>
> Essa mudança será resetada após a reinicialização do sistema. Para garantir que a alteração permaneça permanente, adicione ou atualize o valor de `vm.max_map_count` em **/etc/sysctl.conf**:
>
> ```bash
> vm.max_map_count=262144
> ```
>
2. Clone o repositório:
2. Clone o repositório:
```bash
$ git clone https://github.com/infiniflow/ragflow.git
```
3. Inicie o servidor usando as imagens Docker pré-compiladas:
```bash
$ git clone https://github.com/infiniflow/ragflow.git
```
3. Inicie o servidor usando as imagens Docker pré-compiladas:
> [!CAUTION]
> Todas as imagens Docker são construídas para plataformas x86. Atualmente, não oferecemos imagens Docker para ARM64.
> Se você estiver usando uma plataforma ARM64, por favor, utilize [este guia](https://ragflow.io/docs/dev/build_docker_image) para construir uma imagem Docker compatível com o seu sistema.
> O comando abaixo baixa a edição `v0.21.1` da imagem Docker do RAGFlow. Consulte a tabela a seguir para descrições de diferentes edições do RAGFlow. Para baixar uma edição do RAGFlow diferente da `v0.21.1`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor.
> O comando abaixo baixa a edição`v0.21.1` da imagem Docker do RAGFlow. Consulte a tabela a seguir para descrições de diferentes edições do RAGFlow. Para baixar uma edição do RAGFlow diferente da `v0.21.1`, atualize a variável `RAGFLOW_IMAGE` conforme necessário no **docker/.env** antes de usar `docker compose` para iniciar o servidor.
```bash
```bash
$ cd ragflow/docker
# Use CPU for embedding and DeepDoc tasks:
$ docker compose -f docker-compose.yml up -d
@ -191,43 +196,44 @@ Experimente nossa demo em [https://demo.ragflow.io](https://demo.ragflow.io).
# To use GPU to accelerate embedding and DeepDoc tasks:
# sed -i '1i DEVICE=gpu' .env
# docker compose -f docker-compose.yml up -d
```
| Tag da imagem RAGFlow | Tamanho da imagem (GB) | Possui modelos de incorporação? | Estável? |
| --------------------- | ---------------------- | --------------------------------- | ------------------------------ |
| v0.21.1 | &approx;9 | ✔️ | Lançamento estável |
| v0.21.1-slim | &approx;2 | ❌ | Lançamento estável |
| nightly | &approx;2 | ❌ | Construção noturna instável |
> Observação: A partir da`v0.22.0`, distribuímos apenas a edição slim e não adicionamos mais o sufixo **-slim** às tags das imagens.
4. Verifique o status do servidor após tê-lo iniciado:
```bash
$ docker logs -f docker-ragflow-cpu-1
```
| Tag da imagem RAGFlow | Tamanho da imagem (GB) | Possui modelos de incorporação? | Estável? |
| --------------------- | ---------------------- | ------------------------------- | --------------------------- |
| v0.21.1 | &approx;9 | ✔️ | Lançamento estável |
| v0.21.1-slim | &approx;2 | ❌ | Lançamento estável |
| nightly | &approx;2 | ❌ | Construção noturna instável |
_O seguinte resultado confirma o lançamento bem-sucedido do sistema:_
> Observação: A partir da `v0.22.0`, distribuímos apenas a edição slim e não adicionamos mais o sufixo **-slim** às tags das imagens.
```bash
____ ___ ______ ______ __
/ __ \ / | / ____// ____// /____ _ __
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
4. Verifique o status do servidor após tê-lo iniciado:
* Rodando em todos os endereços (0.0.0.0)
```
```bash
$ docker logs -f docker-ragflow-cpu-1
```
> Se você pular essa etapa de confirmação e acessar diretamente o RAGFlow, seu navegador pode exibir um erro `network anormal`, pois, nesse momento, seu RAGFlow pode não estar totalmente inicializado.
>
5. No seu navegador, insira o endereço IP do seu servidor e faça login no RAGFlow.
_O seguinte resultado confirma o lançamento bem-sucedido do sistema:_
> Com as configurações padrão, você só precisa digitar `http://IP_DO_SEU_MÁQUINA` (**sem** o número da porta), pois a porta HTTP padrão `80` pode ser omitida ao usar as configurações padrão.
>
6. Em [service_conf.yaml.template](./docker/service_conf.yaml.template), selecione a fábrica LLM desejada em `user_default_llm` e atualize o campo `API_KEY` com a chave de API correspondente.
```bash
____ ___ ______ ______ __
/ __ \ / | / ____// ____// /____ _ __
/ /_/ // /| | / / __ / /_ / // __ \| | /| / /
/ _, _// ___ |/ /_/ // __/ / // /_/ /| |/ |/ /
/_/ |_|/_/ |_|\____//_/ /_/ \____/ |__/|__/
* Rodando em todos os endereços (0.0.0.0)
```
> Se você pular essa etapa de confirmação e acessar diretamente o RAGFlow, seu navegador pode exibir um erro `network anormal`, pois, nesse momento, seu RAGFlow pode não estar totalmente inicializado.
5. No seu navegador, insira o endereço IP do seu servidor e faça login no RAGFlow.
> Com as configurações padrão, você só precisa digitar `http://IP_DO_SEU_MÁQUINA` (**sem** o número da porta), pois a porta HTTP padrão `80` pode ser omitida ao usar as configurações padrão.
6. Em [service_conf.yaml.template](./docker/service_conf.yaml.template), selecione a fábrica LLM desejada em `user_default_llm` e atualize o campo `API_KEY` com a chave de API correspondente.
> Consulte [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) para mais informações.
> Consulte [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup) para mais informações.
>
_O show está no ar!_
@ -258,9 +264,9 @@ O RAGFlow usa o Elasticsearch por padrão para armazenar texto completo e vetore
```bash
$ docker compose -f docker/docker-compose.yml down -v
```
Note: `-v` irá deletar os volumes do contêiner, e os dados existentes serão apagados.
2. Defina `DOC_ENGINE` no **docker/.env** para `infinity`.
3. Inicie os contêineres:
```bash
@ -287,7 +293,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```bash
pipx install uv pre-commit
```
2. Clone o código-fonte e instale as dependências Python:
```bash
@ -297,7 +302,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
uv run download_deps.py
pre-commit install
```
3. Inicie os serviços dependentes (MinIO, Elasticsearch, Redis e MySQL) usando Docker Compose:
```bash
@ -309,24 +313,21 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```
127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
```
4. Se não conseguir acessar o HuggingFace, defina a variável de ambiente `HF_ENDPOINT` para usar um site espelho:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```
5. Se o seu sistema operacional não tiver jemalloc, instale-o da seguinte maneira:
```bash
# ubuntu
sudo apt-get install libjemalloc-dev
# centos
sudo yum instalar jemalloc
# mac
sudo brew install jemalloc
```
```bash
# ubuntu
sudo apt-get install libjemalloc-dev
# centos
sudo yum instalar jemalloc
# mac
sudo brew install jemalloc
```
6. Lance o serviço de back-end:
```bash
@ -334,14 +335,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
```
7. Instale as dependências do front-end:
```bash
cd web
npm install
```
8. Lance o serviço de front-end:
```bash
@ -351,13 +350,11 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
_O seguinte resultado confirma o lançamento bem-sucedido do sistema:_
![](https://github.com/user-attachments/assets/0daf462c-a24d-4496-a66f-92533534e187)
9. Pare os serviços de front-end e back-end do RAGFlow após a conclusão do desenvolvimento:
```bash
pkill -f "ragflow_server.py|task_executor.py"
```
```bash
pkill -f "ragflow_server.py|task_executor.py"
```
## 📚 Documentação

View File

@ -43,7 +43,9 @@
<a href="https://demo.ragflow.io">Demo</a>
</h4>
#
<div align="center" style="margin-top:20px;margin-bottom:20px;">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@ -171,22 +173,21 @@
> ```bash
> vm.max_map_count=262144
> ```
>
2. 克隆倉庫:
```bash
$ git clone https://github.com/infiniflow/ragflow.git
```
3. 進入 **docker** 資料夾,利用事先編譯好的 Docker 映像啟動伺服器:
> [!CAUTION]
> 所有 Docker 映像檔都是為 x86 平台建置的。目前,我們不提供 ARM64 平台的 Docker 映像檔。
> 如果您使用的是 ARM64 平台,請使用 [這份指南](https://ragflow.io/docs/dev/build_docker_image) 來建置適合您系統的 Docker 映像檔。
> 執行以下指令會自動下載 RAGFlow slim Docker 映像 `v0.21.1`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.21.1` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。
> 執行以下指令會自動下載 RAGFlow slim Docker 映像 `v0.21.1`。請參考下表查看不同 Docker 發行版的說明。如需下載不同於 `v0.21.1` 的 Docker 映像,請在執行 `docker compose` 啟動服務之前先更新 **docker/.env** 檔案內的 `RAGFLOW_IMAGE` 變數。
```bash
```bash
$ cd ragflow/docker
# Use CPU for embedding and DeepDoc tasks:
$ docker compose -f docker-compose.yml up -d
@ -194,21 +195,21 @@
# To use GPU to accelerate embedding and DeepDoc tasks:
# sed -i '1i DEVICE=gpu' .env
# docker compose -f docker-compose.yml up -d
```
```
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | ------------------------ |
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;2 | ❌ | _Unstable_ nightly build |
| RAGFlow image tag | Image size (GB) | Has embedding models? | Stable? |
| ----------------- | --------------- | --------------------- | -------------------------- |
| v0.21.1 | &approx;9 | ✔️ | Stable release |
| v0.21.1-slim | &approx;2 | ❌ | Stable release |
| nightly | &approx;2 | ❌ | _Unstable_ nightly build |
> 注意:自 `v0.22.0` 起,我們僅發佈 slim 版本,並且不再在映像標籤後附加 **-slim** 後綴。
> 注意:自 `v0.22.0` 起,我們僅發佈 slim 版本,並且不再在映像標籤後附加 **-slim** 後綴。
> [!TIP]
> 如果你遇到 Docker 映像檔拉不下來的問題,可以在 **docker/.env** 檔案內根據變數 `RAGFLOW_IMAGE` 的註解提示選擇華為雲或阿里雲的對應映像。
>
> - 華為雲鏡像名:`swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow`
> - 阿里雲鏡像名:`registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow`
> [!TIP]
> 如果你遇到 Docker 映像檔拉不下來的問題,可以在 **docker/.env** 檔案內根據變數 `RAGFLOW_IMAGE` 的註解提示選擇華為雲或阿里雲的對應映像。
>
> - 華為雲鏡像名:`swr.cn-north-4.myhuaweicloud.com/infiniflow/ragflow`
> - 阿里雲鏡像名:`registry.cn-hangzhou.aliyuncs.com/infiniflow/ragflow`
4. 伺服器啟動成功後再次確認伺服器狀態:
@ -229,12 +230,15 @@
```
> 如果您跳過這一步驟系統確認步驟就登入 RAGFlow你的瀏覽器有可能會提示 `network anormal` 或 `網路異常`,因為 RAGFlow 可能並未完全啟動成功。
>
5. 在你的瀏覽器中輸入你的伺服器對應的 IP 位址並登入 RAGFlow。
> 上面這個範例中,您只需輸入 http://IP_OF_YOUR_MACHINE 即可:未改動過設定則無需輸入連接埠(預設的 HTTP 服務連接埠 80
>
6. 在 [service_conf.yaml.template](./docker/service_conf.yaml.template) 檔案的 `user_default_llm` 欄位設定 LLM factory並在 `API_KEY` 欄填入和你選擇的大模型相對應的 API key。
> 詳見 [llm_api_key_setup](https://ragflow.io/docs/dev/llm_api_key_setup)。
>
_好戲開始接著奏樂接著舞 _
@ -252,7 +256,7 @@
> [./docker/README](./docker/README.md) 解釋了 [service_conf.yaml.template](./docker/service_conf.yaml.template) 用到的環境變數設定和服務配置。
如需更新預設的 HTTP 服務連接埠(80), 可以在[docker-compose.yml](./docker/docker-compose.yml) 檔案中將配置`80:80` 改為`<YOUR_SERVING_PORT>:80` 。
如需更新預設的 HTTP 服務連接埠(80), 可以在[docker-compose.yml](./docker/docker-compose.yml) 檔案中將配置 `80:80` 改為 `<YOUR_SERVING_PORT>:80` 。
> 所有系統配置都需要透過系統重新啟動生效:
>
@ -269,10 +273,9 @@ RAGFlow 預設使用 Elasticsearch 儲存文字和向量資料. 如果要切換
```bash
$ docker compose -f docker/docker-compose.yml down -v
```
Note: `-v` 將會刪除 docker 容器的 volumes已有的資料會被清空。
2. 設定 **docker/.env** 目錄中的 `DOC_ENGINE` 為 `infinity`.
3. 啟動容器:
```bash
@ -300,7 +303,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
pipx install uv pre-commit
export UV_INDEX=https://mirrors.aliyun.com/pypi/simple
```
2. 下載原始碼並安裝 Python 依賴:
```bash
@ -310,7 +312,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
uv run download_deps.py
pre-commit install
```
3. 透過 Docker Compose 啟動依賴的服務MinIO, Elasticsearch, Redis, and MySQL
```bash
@ -322,13 +323,11 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
```
127.0.0.1 es01 infinity mysql minio redis sandbox-executor-manager
```
4. 如果無法存取 HuggingFace可以把環境變數 `HF_ENDPOINT` 設為對應的鏡像網站:
```bash
export HF_ENDPOINT=https://hf-mirror.com
```
5. 如果你的操作系统没有 jemalloc请按照如下方式安装
```bash
@ -339,7 +338,6 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
# mac
sudo brew install jemalloc
```
6. 啟動後端服務:
```bash
@ -347,14 +345,12 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
export PYTHONPATH=$(pwd)
bash docker/launch_backend_service.sh
```
7. 安裝前端依賴:
```bash
cd web
npm install
```
8. 啟動前端服務:
```bash
@ -364,15 +360,16 @@ docker build --platform linux/amd64 -f Dockerfile -t infiniflow/ragflow:nightly
以下界面說明系統已成功啟動_
![](https://github.com/user-attachments/assets/0daf462c-a24d-4496-a66f-92533534e187)
```
```
9. 開發完成後停止 RAGFlow 前端和後端服務:
```bash
pkill -f "ragflow_server.py|task_executor.py"
```
## 📚 技術文檔
- [Quickstart](https://ragflow.io/docs/dev/)

View File

@ -43,7 +43,9 @@
<a href="https://demo.ragflow.io">Demo</a>
</h4>
#
<div align="center" style="margin-top:20px;margin-bottom:20px;">
<img src="https://raw.githubusercontent.com/infiniflow/ragflow-docs/refs/heads/image/image/ragflow-octoverse.png" width="1200"/>
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9064" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9064" alt="infiniflow%2Fragflow | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>

View File

@ -38,6 +38,13 @@ export default defineConfig({
{ from: 'node_modules/monaco-editor/min/vs/', to: 'dist/vs/' },
],
proxy: [
{
context: ['/api/v1/admin'],
target: 'http://127.0.0.1:9381/',
changeOrigin: true,
ws: true,
logger: console,
},
{
context: ['/api', '/v1'],
target: 'http://127.0.0.1:9380/',

View File

@ -39,7 +39,8 @@ const ScrollArea = React.forwardRef<
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));

View File

@ -278,8 +278,8 @@ export default {
tocExtractionTip:
" For existing chunks, generate a hierarchical table of contents (one directory per file). During queries, when Directory Enhancement is activated, the system will use a large model to determine which directory items are relevant to the user's question, thereby identifying the relevant chunks.",
deleteGenerateModalContent: `
<p>Deleting the generated <strong class='text-text-primary'>{{type}}</strong> results
will remove all derived entities and relationships from this dataset.
<p>Deleting the generated <strong class='text-text-primary'>{{type}}</strong> results
will remove all derived entities and relationships from this dataset.
Your original files will remain intact.<p>
<br/>
Do you want to continue?
@ -1813,15 +1813,15 @@ Important structured information may include: names, dates, locations, events, k
</ul>`,
changeStepModalTitle: 'Step Switch Warning',
changeStepModalContent: `
<p>You are currently editing the results of this stage.</p>
<p>If you switch to a later stage, your changes will be lost. </p>
<p>You are currently editing the results of this stage.</p>
<p>If you switch to a later stage, your changes will be lost. </p>
<p>To keep them, please click Rerun to re-run the current stage.</p> `,
changeStepModalConfirmText: 'Switch Anyway',
changeStepModalCancelText: 'Cancel',
unlinkPipelineModalTitle: 'Unlink Ingestion pipeline',
unlinkPipelineModalContent: `
<p>Once unlinked, this Dataset will no longer be connected to the current Ingestion pipeline.</p>
<p>Files that are already being parsed will continue until completion</p>
<p>Once unlinked, this Dataset will no longer be connected to the current Ingestion pipeline.</p>
<p>Files that are already being parsed will continue until completion</p>
<p>Files that are not yet parsed will no longer be processed</p> <br/>
<p>Are you sure you want to proceed?</p> `,
unlinkPipelineModalConfirmText: 'Unlink',
@ -1837,5 +1837,125 @@ Important structured information may include: names, dates, locations, events, k
processingFailedTip: 'Total failed processes',
processing: 'Processing',
},
admin: {
loginTitle: 'RAGFlow ADMIN',
title: 'RAGFlow admin',
confirm: 'Confirm',
close: 'Close',
yes: 'Yes',
no: 'No',
delete: 'Delete',
cancel: 'Cancel',
reset: 'Reset',
import: 'Import',
description: 'Description',
noDescription: 'No description',
resourceType: {
dataset: 'Dataset',
chat: 'Chat',
agent: 'Agent',
search: 'Search',
file: 'File',
team: 'Team',
memory: 'Memory',
},
permissionType: {
enable: 'Enable',
read: 'Read',
write: 'Write',
share: 'Share',
},
serviceStatus: 'Service status',
userManagement: 'User management',
registrationWhitelist: 'Registration whitelist',
roles: 'Roles',
monitoring: 'Monitoring',
active: 'Active',
inactive: 'Inactive',
enable: 'Enable',
disable: 'Disable',
all: 'All',
actions: 'Actions',
newUser: 'New User',
email: 'Email',
name: 'Name',
nickname: 'Nickname',
status: 'Status',
id: 'ID',
serviceType: 'Service type',
host: 'Host',
port: 'Port',
role: 'Role',
user: 'User',
superuser: 'Superuser',
createTime: 'Create time',
lastLoginTime: 'Last login time',
lastUpdateTime: 'Last update time',
isAnonymous: 'Is Anonymous',
deleteUser: 'Delete user',
deleteUserConfirmation: 'Are you sure you want to delete this user?',
createNewUser: 'Create new user',
changePassword: 'Change password',
newPassword: 'New password',
confirmNewPassword: 'Confirm new password',
password: 'Password',
confirmPassword: 'Confirm password',
invalidEmail: 'Please input a valid email address!',
passwordRequired: 'Please input your password!',
passwordMinLength: 'Password must be more than 8 characters.',
confirmPasswordRequired: 'Please confirm your password!',
confirmPasswordDoNotMatch: 'The password that you entered do not match!',
read: 'Read',
write: 'Write',
share: 'Share',
create: 'Create',
extraInfo: 'Extra information',
serviceDetail: `Service {{name}} detail`,
whitelistManagement: 'Whitelist management',
exportAsExcel: 'Export Excel',
importFromExcel: 'Import Excel',
createEmail: 'Create email',
deleteEmail: 'Delete email',
editEmail: 'Edit email',
deleteWhitelistEmailConfirmation:
'Are you sure you want to delete this email from whitelist? This action cannot be undone.',
importWhitelist: 'Import whitelist (excel)',
importSelectExcelFile: 'Excel file (.xlsx)',
importOverwriteExistingEmails: 'Overwrite existing emails',
importInvalidExcelFile: 'Please select a valid Excel file',
importFileRequired: 'Please select a file to import',
importFileTips:
'File must contain a single header column named <code>email</code>.',
chunkNum: 'Chunks',
docNum: 'Documents',
tokenNum: 'Tokens used',
language: 'Language',
createDate: 'Create date',
updateDate: 'Update date',
permission: 'Permission',
agentTitle: 'Agent title',
canvasCategory: 'Canvas category',
newRole: 'New Role',
addNewRole: 'Add new role',
roleName: 'Role name',
resources: 'Resources',
},
},
};

View File

@ -1,14 +1,24 @@
import { Routes } from '@/routes';
import { Button, Result } from 'antd';
import { history } from 'umi';
import { history, useLocation } from 'umi';
const NoFoundPage = () => {
const location = useLocation();
return (
<Result
status="404"
title="404"
subTitle="Page not found, please enter a correct address."
extra={
<Button type="primary" onClick={() => history.push('/')}>
<Button
type="primary"
onClick={() => {
history.push(
location.pathname.startsWith(Routes.Admin) ? Routes.Admin : '/',
);
}}
>
Business
</Button>
}

View File

@ -0,0 +1,13 @@
import { IS_ENTERPRISE } from '../utils';
export default function EnterpriseFeature({
children,
}: {
children: () => React.ReactNode;
}) {
return IS_ENTERPRISE
? typeof children === 'function'
? children()
: children
: null;
}

View File

@ -0,0 +1,47 @@
import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
import { ThemeEnum } from '@/constants/common';
import { cn } from '@/lib/utils';
import { Root, Thumb } from '@radix-ui/react-switch';
import { LucideMoon, LucideSun } from 'lucide-react';
import { forwardRef } from 'react';
const ThemeSwitch = forwardRef<
React.ElementRef<typeof Root>,
React.ComponentPropsWithoutRef<typeof Root>
>(({ className, ...props }, ref) => {
const { setTheme } = useTheme();
const isDark = useIsDarkTheme();
return (
<Root
ref={ref}
className={cn('relative rounded-full')}
{...props}
checked={isDark}
onCheckedChange={(value) =>
setTheme(value ? ThemeEnum.Dark : ThemeEnum.Light)
}
>
<div className="px-3 py-2 rounded-full border border-border-button bg-bg-card transition-[background-color] duration-200">
<div className="flex items-center justify-between gap-4 relative z-[1] text-text-disabled transition-[text-color] duration-200">
<LucideSun
className={cn('size-[1em]', !isDark && 'text-text-primary')}
/>
<LucideMoon
className={cn('size-[1em]', isDark && 'text-text-primary')}
/>
</div>
</div>
<Thumb
className={cn(
'absolute top-0 left-0 w-[calc(50%+.25rem)] h-full rounded-full bg-bg-base border border-border-button',
'transition-all duration-200',
{ 'left-[calc(50%-.25rem)]': isDark },
)}
/>
</Root>
);
});
export default ThemeSwitch;

View File

@ -0,0 +1,150 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback, useId, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
interface ChangePasswordFormData {
newPassword: string;
confirmPassword: string;
}
interface ChangePasswordFormProps {
id: string;
form: ReturnType<typeof useForm<ChangePasswordFormData>>;
email?: string;
onSubmit?: (data: ChangePasswordFormData) => void;
}
export const ChangePasswordForm = ({
id,
form,
email,
onSubmit = () => {},
}: ChangePasswordFormProps) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
id={id}
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
{/* Email field (readonly) */}
<div>
<FormLabel className="text-sm font-medium">
{t('admin.email')}
</FormLabel>
<Input
value={email}
readOnly
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
/>
</div>
{/* New password field */}
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.newPassword')}
</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('admin.newPassword')}
autoComplete="new-password"
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Confirm password field */}
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.confirmNewPassword')}
</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('admin.confirmNewPassword')}
autoComplete="new-password"
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
};
// Export the form validation state for parent component
function useChangePasswordForm() {
const { t } = useTranslation();
const id = useId();
const schema = useMemo(() => {
return z
.object({
newPassword: z
.string()
.min(8, { message: t('admin.passwordMinLength') }),
confirmPassword: z
.string()
.min(8, { message: t('admin.confirmPasswordRequired') }),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: t('admin.confirmPasswordDoNotMatch'),
path: ['confirmPassword'],
});
}, [t]);
const form = useForm<ChangePasswordFormData>({
defaultValues: {
newPassword: '',
confirmPassword: '',
},
resolver: zodResolver(schema),
});
const FormComponent = useCallback(
(props: Partial<ChangePasswordFormProps>) => (
<ChangePasswordForm id={id} form={form} {...props} />
),
[id, form],
);
return {
schema,
id,
form,
FormComponent,
};
}
export default useChangePasswordForm;

View File

@ -0,0 +1,102 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback, useId, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
interface CreateEmailFormData {
email: string;
}
interface CreateEmailFormProps {
id: string;
form: ReturnType<typeof useForm<CreateEmailFormData>>;
onSubmit?: (data: CreateEmailFormData) => void;
}
export const CreateEmailForm = ({
id,
form,
onSubmit = () => {},
}: CreateEmailFormProps) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
id={id}
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
{/* Email field */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.email')}
</FormLabel>
<FormControl>
<Input
placeholder="name@example.com"
autoComplete="email"
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
};
// Export the form validation state for parent component
function useCreateEmailForm(props?: {
defaultValues: Partial<CreateEmailFormData>;
}) {
const { t } = useTranslation();
const id = useId();
const schema = useMemo(() => {
return z.object({
email: z.string().email({ message: t('admin.invalidEmail') }),
});
}, [t]);
const form = useForm<CreateEmailFormData>({
defaultValues: {
email: '',
...(props?.defaultValues ?? {}),
},
resolver: zodResolver(schema),
});
const FormComponent = useCallback(
(props: Partial<CreateEmailFormProps>) => (
<CreateEmailForm id={id} form={form} {...props} />
),
[id, form],
);
return {
schema,
id,
form,
FormComponent,
};
}
export default useCreateEmailForm;

View File

@ -0,0 +1,155 @@
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback, useId, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { z } from 'zod';
interface ImportExcelFormData {
file: FileList;
overwriteExisting: boolean;
}
interface ImportExcelFormProps {
id: string;
form: ReturnType<typeof useForm<ImportExcelFormData>>;
onSubmit?: (data: ImportExcelFormData) => void;
}
export const ImportExcelForm = ({
id,
form,
onSubmit = () => {},
}: ImportExcelFormProps) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
id={id}
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
{/* File input field */}
<FormField
control={form.control}
name="file"
render={({ field: { onChange, value, ...field } }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.importSelectExcelFile')}
</FormLabel>
<FormControl>
<Input
type="file"
accept=".xlsx"
className="mt-2 px-3 h-10 bg-bg-input border-border-button file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-bg-accent file:text-text-primary hover:file:bg-bg-accent/80"
onChange={(e) => {
const files = e.target.files;
onChange(files);
}}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Overwrite checkbox */}
<FormField
control={form.control}
name="overwriteExisting"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2 text-sm font-medium">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
{t('admin.importOverwriteExistingEmails')}
</FormLabel>
</FormItem>
)}
/>
<p className="text-xs text-text-secondary">
<Trans
i18nKey="admin.importFileTips"
components={{ code: <code /> }}
/>
</p>
</form>
</Form>
);
};
// Export the form validation state for parent component
function useImportExcelForm() {
const { t } = useTranslation();
const id = useId();
const schema = useMemo(() => {
return z.object({
file: z
.any()
.refine((files) => files && files.length > 0, {
message: t('admin.importFileRequired'),
})
.refine(
(files) => {
if (!files || files.length === 0) return false;
const [file] = files;
return (
file.type ===
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
// || file.type === 'application/vnd.ms-excel'
file.name.endsWith('.xlsx')
);
// || file.name.endsWith('.xls');
},
{
message: t('admin.invalidExcelFile'),
},
),
overwriteExisting: z.boolean().optional(),
});
}, [t]);
const form = useForm<ImportExcelFormData>({
defaultValues: {
file: undefined,
overwriteExisting: false,
},
resolver: zodResolver(schema),
});
const FormComponent = useCallback(
(props: Partial<ImportExcelFormProps>) => (
<ImportExcelForm id={id} form={form} {...props} />
),
[id, form],
);
return {
schema,
id,
form,
FormComponent,
};
}
export default useImportExcelForm;

View File

@ -0,0 +1,207 @@
import { Card, CardContent } from '@/components/ui/card';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs-underlined';
import { AdminService, listResources } from '@/services/admin-service';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useId, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
interface CreateRoleFormData {
name: string;
description: string;
permissions: Record<string, AdminService.PermissionData>;
}
interface CreateRoleFormProps {
id: string;
form: ReturnType<typeof useForm<CreateRoleFormData>>;
onSubmit?: (data: CreateRoleFormData) => void;
}
const PERMISSION_TYPES = ['enable', 'read', 'write', 'share'] as const;
export const CreateRoleForm = ({
id,
form,
onSubmit = () => {},
}: CreateRoleFormProps) => {
const { t } = useTranslation();
const { data: resourceTypes } = useQuery({
queryKey: ['admin/resourceTypes'],
queryFn: async () => (await listResources()).data.data.resource_types,
});
return (
<Form {...form}>
<form
id={id}
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
{/* Role name field */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium" required>
{t('admin.roleName')}
</FormLabel>
<FormControl>
<Input
placeholder={t('admin.roleName')}
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Role description field */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.description')}
</FormLabel>
<FormControl>
<Input
placeholder={t('admin.description')}
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
{/* Permissions section */}
<div>
<Label>{t('admin.resources')}</Label>
<Tabs defaultValue={resourceTypes?.[0]} className="w-full mt-2">
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
{resourceTypes?.map((resourceType) => (
<TabsTrigger
key={resourceType}
value={resourceType}
className="text-text-secondary border-border-button dark:data-[state=active]:bg-bg-input"
>
{t(`admin.resourceType.${resourceType}`)}
</TabsTrigger>
))}
</TabsList>
{resourceTypes?.map((resourceType) => (
<TabsContent
key={resourceType}
value={resourceType}
className="space-y-4"
>
<Card className="border-0 bg-bg-card">
<CardContent className="p-6">
<div className="grid grid-cols-4 gap-4">
{PERMISSION_TYPES.map((permissionType) => (
<FormField
key={permissionType}
name={`permissions.${resourceType}.${permissionType}`}
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
{t(`admin.permissionType.${permissionType}`)}
</FormLabel>
<FormControl>
<Switch
{...field}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
))}
</div>
</CardContent>
</Card>
</TabsContent>
))}
</Tabs>
</div>
</form>
</Form>
);
};
// Export the form validation state for parent component
function useCreateRoleForm(props?: {
defaultValues: Partial<CreateRoleFormData>;
}) {
const { t } = useTranslation();
const id = useId();
const schema = useMemo(() => {
return z.object({
name: z.string().min(1, { message: 'Role name is required' }),
description: z.string().optional(),
permissions: z.record(
z.string(),
z.object({
enable: z.boolean(),
read: z.boolean(),
write: z.boolean(),
share: z.boolean(),
}),
),
});
}, [t]);
const form = useForm<CreateRoleFormData>({
defaultValues: {
name: '',
description: '',
permissions: {},
...(props?.defaultValues ?? {}),
},
resolver: zodResolver(schema),
});
const FormComponent = useCallback(
(props: Partial<CreateRoleFormProps>) => (
<CreateRoleForm id="create-role-form" form={form} {...props} />
),
[form],
);
return {
schema,
id,
form,
FormComponent,
};
}
export default useCreateRoleForm;

View File

@ -0,0 +1,215 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useId, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { listRoles } from '@/services/admin-service';
import EnterpriseFeature from '../components/enterprise-feature';
interface CreateUserFormData {
email: string;
password: string;
confirmPassword: string;
role?: string;
}
interface CreateUserFormProps {
id: string;
form: ReturnType<typeof useForm<CreateUserFormData>>;
onSubmit?: (data: CreateUserFormData) => void;
}
export const CreateUserForm = ({
id,
form,
onSubmit = () => {},
}: CreateUserFormProps) => {
const { t } = useTranslation();
const { data: roleList } = useQuery({
queryKey: ['admin/listRoles'],
queryFn: async () => (await listRoles()).data.data.roles,
});
return (
<Form {...form}>
<form
id={id}
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
{/* Email field (editable) */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.email')}
</FormLabel>
<FormControl>
<Input
placeholder={t('admin.email')}
autoComplete="username"
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Password field */}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.password')}
</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('admin.password')}
autoComplete="new-password"
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Confirm password field */}
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.confirmPassword')}
</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('admin.confirmPassword')}
autoComplete="new-password"
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<EnterpriseFeature>
{/* Role field */}
{() => (
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
{t('admin.role')}
</FormLabel>
<FormControl>
<Select {...field}>
<SelectTrigger className="w-full h-10">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-bg-base">
<SelectGroup>
{roleList?.map((role) => (
<SelectItem key={role.id} value={role.role_name}>
{role.role_name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
)}
</EnterpriseFeature>
</form>
</Form>
);
};
// Export the form validation state for parent component
function useCreateUserForm(props?: {
defaultValues: Partial<CreateUserFormData>;
}) {
const { t } = useTranslation();
const id = useId();
const schema = useMemo(() => {
return z
.object({
email: z.string().email({ message: t('admin.invalidEmail') }),
password: z.string().min(6, { message: t('admin.passwordMinLength') }),
confirmPassword: z
.string()
.min(1, { message: t('admin.confirmPasswordRequired') }),
role: z.string().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
message: t('admin.confirmPasswordDoNotMatch'),
path: ['confirmPassword'],
});
}, [t]);
const form = useForm<CreateUserFormData>({
defaultValues: {
email: '',
password: '',
confirmPassword: '',
...(props?.defaultValues ?? {}),
},
resolver: zodResolver(schema),
});
const FormComponent = useCallback(
(props: Partial<CreateUserFormProps>) => (
<CreateUserForm id={id} form={form} {...props} />
),
[id, form],
);
return {
schema,
id,
form,
FormComponent,
};
}
export default useCreateUserForm;

View File

@ -0,0 +1,265 @@
import Spotlight from '@/components/spotlight';
import { ButtonLoading } from '@/components/ui/button';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Authorization } from '@/constants/authorization';
import { useAuth } from '@/hooks/auth-hooks';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import adminService from '@/services/admin-service';
import { rsaPsw } from '@/utils';
import authorizationUtil from '@/utils/authorization-util';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { AxiosResponseHeaders } from 'axios';
import { LucideEye, LucideEyeOff } from 'lucide-react';
import { useEffect, useId, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
import { z } from 'zod';
import { BgSvg } from '../login-next/bg';
import ThemeSwitch from './components/theme-switch';
function AdminLogin() {
const navigate = useNavigate();
const { t } = useTranslation('translation', { keyPrefix: 'login' });
const { isLogin } = useAuth();
const [showPassword, setShowPassword] = useState(false);
const { isPending: signLoading, mutateAsync: login } = useMutation({
mutationKey: ['adminLogin'],
mutationFn: async (params: { email: string; password: string }) => {
const request = await adminService.login(params);
const { data: req, headers } = request;
if (req.code === 0) {
const authorization = (headers as AxiosResponseHeaders)?.get(
Authorization,
);
const token = req.data.access_token;
const userInfo = {
avatar: req.data.avatar,
name: req.data.nickname,
email: req.data.email,
};
authorizationUtil.setItems({
Authorization: authorization as string,
Token: token,
userInfo: JSON.stringify(userInfo),
});
}
return req;
},
});
const loading = signLoading;
useEffect(() => {
if (isLogin) {
navigate(Routes.AdminServices);
}
}, [isLogin, navigate]);
const FormSchema = z.object({
email: z
.string()
.email()
.min(1, { message: t('emailPlaceholder') }),
password: z.string().min(1, { message: t('passwordPlaceholder') }),
remember: z.boolean().optional(),
});
const formId = useId();
const form = useForm({
defaultValues: {
email: '',
password: '',
remember: false,
},
resolver: zodResolver(FormSchema),
});
const onCheck: SubmitHandler<z.infer<typeof FormSchema>> = async (params) => {
try {
const rsaPassWord = rsaPsw(params.password) as string;
const { code } = await login({
email: `${params.email}`.trim(),
password: rsaPassWord,
});
if (code === 0) {
navigate('/admin/services');
}
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
};
return (
<div className="relative w-screen h-screen">
<Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" />
<Spotlight
opcity={0.3}
coverage={12}
X="10%"
Y="-10%"
color="rgb(128, 255, 248)"
/>
<Spotlight
opcity={0.3}
coverage={12}
X="90%"
Y="-10%"
color="rgb(128, 255, 248)"
/>
<BgSvg />
<div className="absolute top-3 left-0 w-full">
<div className="absolute mt-12 ml-12 flex items-center">
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
<span className="text-xl font-bold">RAGFlow</span>
</div>
<h1 className="mt-[6.5rem] text-4xl font-medium text-center mb-12">
{t('loginTitle', { keyPrefix: 'admin' })}
</h1>
</div>
<div className="flex items-center justify-center w-screen h-screen">
<div className="w-full max-w-[540px]">
<Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button">
<CardContent className="px-10 pt-14 pb-10">
<Form {...form}>
<form
id={formId}
className="space-y-8 text-text-primary"
onSubmit={form.handleSubmit(onCheck)}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input
className="h-10 px-2.5"
placeholder={t('emailPlaceholder')}
autoComplete="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<div className="relative">
<Input
className="h-10 px-2.5"
type={showPassword ? 'text' : 'password'}
placeholder={t('passwordPlaceholder')}
autoComplete="password"
{...field}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<LucideEyeOff className="h-4 w-4 text-gray-500" />
) : (
<LucideEye className="h-4 w-4 text-gray-500" />
)}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem className="!mt-5">
<FormLabel
className={cn(
'flex items-center hover:text-text-primary',
field.value
? 'text-text-primary'
: 'text-text-disabled',
)}
>
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<span className="ml-2">{t('rememberMe')}</span>
</FormLabel>
</FormItem>
)}
/>
</form>
</Form>
</CardContent>
<CardFooter className="px-10 pt-8 pb-14">
<ButtonLoading
form={formId}
size="lg"
className="
w-full h-10
bg-metallic-gradient border-b-[#00BEB4] border-b-2
hover:bg-metallic-gradient hover:border-b-[#02bcdd]
"
type="submit"
loading={loading}
>
{t('login')}
</ButtonLoading>
</CardFooter>
</Card>
<div className="mt-8 flex justify-center">
<ThemeSwitch />
</div>
</div>
</div>
</div>
);
}
export default AdminLogin;

View File

@ -0,0 +1,133 @@
import { Button } from '@/components/ui/button';
import message from '@/components/ui/message';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import adminService from '@/services/admin-service';
import authorizationUtil from '@/utils/authorization-util';
import { useMutation } from '@tanstack/react-query';
import {
LucideMonitor,
LucideServerCrash,
LucideSquareUserRound,
LucideUserCog,
LucideUserStar,
} from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { NavLink, Outlet, useLocation, useNavigate } from 'umi';
import ThemeSwitch from './components/theme-switch';
import { IS_ENTERPRISE } from './utils';
const AdminLayout = () => {
const { t } = useTranslation();
const { pathname } = useLocation();
const navigate = useNavigate();
const navItems = useMemo(
() => [
{
path: Routes.AdminServices,
name: t('admin.serviceStatus'),
icon: <LucideServerCrash className="size-[1em]" />,
},
{
path: Routes.AdminUserManagement,
name: t('admin.userManagement'),
icon: <LucideUserCog className="size-[1em]" />,
},
...(IS_ENTERPRISE
? [
{
path: Routes.AdminWhitelist,
name: t('admin.registrationWhitelist'),
icon: <LucideUserStar className="size-[1em]" />,
},
{
path: Routes.AdminRoles,
name: t('admin.roles'),
icon: <LucideSquareUserRound className="size-[1em]" />,
},
{
path: Routes.AdminMonitoring,
name: t('admin.monitoring'),
icon: <LucideMonitor className="size-[1em]" />,
},
]
: []),
],
[t],
);
const {
data,
isPending,
mutateAsync: logout,
} = useMutation({
mutationKey: ['adminLogout'],
mutationFn: async () => {
await adminService.logout();
message.success(t('message.logout'));
authorizationUtil.removeAll();
navigate(Routes.Admin);
},
});
return (
<main className="w-screen h-screen flex flex-row px-6 pt-12 pb-6 dark:*:focus-visible:ring-white">
<aside className="w-[28rem] mr-6 flex flex-col gap-6">
<div className="flex items-center mb-6">
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
<span className="text-xl font-bold">{t('admin.title')}</span>
</div>
<nav>
<ul className="space-y-4">
{navItems.map((it) => (
<li key={it.path}>
<NavLink
to={it.path}
className={cn(
'px-4 py-3 rounded-lg',
'text-base w-full flex items-center justify-start text-text-secondary',
'hover:bg-bg-card focus:bg-bg-card focus-visible:bg-bg-card',
'hover:text-text-primary focus:text-text-primary focus-visible:text-text-primary',
'active:text-text-primary',
{
'bg-bg-card text-text-primary':
it.path && pathname.startsWith(it.path),
},
)}
>
{it.icon}
<span className="ml-3">{it.name}</span>
</NavLink>
</li>
))}
</ul>
</nav>
<div className="mt-auto space-y-4">
<div className="text-right">
<ThemeSwitch />
</div>
<Button
size="lg"
variant="transparent"
className="block w-full dark:border-border-button"
onClick={() => logout()}
>
{t('header.logout')}
</Button>
</div>
</aside>
<section className="w-full h-full">
<Outlet />
</section>
</main>
);
};
export default AdminLayout;

View File

@ -0,0 +1,13 @@
import { Card, CardContent } from '@/components/ui/card';
function AdminMonitoring() {
return (
<Card className="h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<CardContent className="size-full p-0">
<iframe />
</CardContent>
</Card>
);
}
export default AdminMonitoring;

View File

@ -0,0 +1,229 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { LoadingButton } from '@/components/ui/loading-button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Switch } from '@/components/ui/switch';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs-underlined';
import { LucideEdit3, LucideTrash2, LucideUserPlus } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { listRolesWithPermission } from '@/services/admin-service';
import useCreateRoleForm from './forms/role-form';
// #region FAKE DATA
function _pickRandom<T extends unknown>(arr: T[]): T | void {
return arr[Math.floor(Math.random() * arr.length)];
}
const PSEUDO_TABLE_ITEMS = Array.from({ length: 20 }, () => ({
id: Math.random().toString(36).slice(2, 8),
name: 'Ahaha',
description: 'Ahaha description',
permissions: {
dataset: {
enable: _pickRandom([true, false]),
read: _pickRandom([true, false]),
write: _pickRandom([true, false]),
share: _pickRandom([true, false]),
},
agent: {
enable: _pickRandom([true, false]),
read: _pickRandom([true, false]),
write: _pickRandom([true, false]),
share: _pickRandom([true, false]),
},
},
}));
// #endregion
function AdminRoles() {
const { t } = useTranslation();
const [isAddRoleModalOpen, setIsAddRoleModalOpen] = useState(false);
const { data: roleList } = useQuery({
queryKey: ['admin/listRolesWithPermission'],
queryFn: async () => (await listRolesWithPermission()).data.data.roles,
});
const createRoleForm = useCreateRoleForm();
const handleAddRole = (data: any) => {
console.log('New role data:', data);
// TODO: Implement actual role creation logic
createRoleForm.form.reset();
setIsAddRoleModalOpen(false);
};
return (
<>
<Card className="h-full border border-border-button bg-transparent rounded-xl">
<ScrollArea className="size-full">
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
<CardTitle>{t('admin.roles')}</CardTitle>
<Button
className="h-10 px-4"
onClick={() => setIsAddRoleModalOpen(true)}
>
<LucideUserPlus />
{t('admin.newRole')}
</Button>
</CardHeader>
<CardContent className="space-y-6">
{roleList?.map((role) => {
const resources = Object.entries(role.permissions);
return (
<Card
key={role.id}
className="group border border-border-default bg-transparent hover:bg-bg-card transition-color duration-150"
>
<CardHeader className="space-y-0 flex flex-row items-center border-b border-border-button">
<div className="space-y-1.5">
<CardTitle className="font-normal text-xl">
{role.role_name}
</CardTitle>
<div className="text-sm text-text-secondary">
{role.description || (
<i className="text-muted-foreground">
{t('admin.noDescription')}
</i>
)}
<Button
variant="transparent"
className="ml-2 p-0 border-0 size-[1em] align-middle opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
>
<LucideEdit3 className="!size-[1em]" />
</Button>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="ml-auto opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
>
<LucideTrash2 />
</Button>
</CardHeader>
<CardContent className="p-6">
<Tabs
className="h-full flex flex-col"
defaultValue={resources[0]?.[0]}
>
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
{resources.map(([name]) => (
<TabsTrigger
key={name}
value={name}
className="border-border-button dark:data-[state=active]:bg-bg-input"
>
{t(`admin.resourceType.${name}`)}
</TabsTrigger>
))}
</TabsList>
{resources.map(([name, permission]) => (
<TabsContent key={name} value={name}>
<div className="flex gap-8">
<Label className="flex items-center gap-2">
<Switch
checked={!!permission.enable}
onCheckedChange={console.log}
/>
{t('admin.enable')}
</Label>
<Label className="flex items-center gap-2">
<Switch
checked={!!permission.read}
onCheckedChange={() => {}}
/>
{t('admin.read')}
</Label>
<Label className="flex items-center gap-2">
<Switch
checked={!!permission.write}
onCheckedChange={() => {}}
/>
{t('admin.write')}
</Label>
<Label className="flex items-center gap-2">
<Switch
checked={!!permission.share}
onCheckedChange={() => {}}
/>
{t('admin.share')}
</Label>
</div>
</TabsContent>
))}
</Tabs>
</CardContent>
</Card>
);
})}
</CardContent>
</ScrollArea>
</Card>
{/* Add Role Modal */}
<Dialog open={isAddRoleModalOpen} onOpenChange={setIsAddRoleModalOpen}>
<DialogContent className="max-w-2xl p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.addNewRole')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4">
<createRoleForm.FormComponent onSubmit={handleAddRole} />
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => setIsAddRoleModalOpen(false)}
>
{t('admin.cancel')}
</Button>
<LoadingButton
type="submit"
form={createRoleForm.id}
className="px-4 h-10"
variant="default"
>
{t('admin.confirm')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default AdminRoles;

View File

@ -0,0 +1,86 @@
import { isPlainObject } from 'lodash';
import { useMemo } from 'react';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
interface ServiceDetailProps {
content?: any;
}
function ServiceDetail({ content }: ServiceDetailProps) {
const contentElement = useMemo(() => {
if (Array.isArray(content) && content.every(isPlainObject)) {
const headers = Object.keys(content[0]);
return (
<Table rootClassName="min-w-max">
<TableHeader>
<TableRow>
{headers.map((header) => (
<TableHead key={header}>{header}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{content.map((item) => (
<TableRow key={item.id as string}>
{headers.map((header: string) => (
<TableCell key={header}>{item[header] as string}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
);
}
if (isPlainObject(content)) {
return (
<dl className="grid grid-cols-[auto,1fr] border border-card rounded-xl overflow-hidden bg-bg-card">
{Object.entries<any>(content).map(([key, value]) => (
<div key={key} className="contents">
<dt className="px-3 py-2 bg-bg-card">
<pre>
<code>{key}</code>
</pre>
</dt>
<dd className="px-3 py-2">
<pre>
<code>{JSON.stringify(value)}</code>
</pre>
</dd>
</div>
))}
</dl>
);
}
if (typeof content === 'string') {
return (
<div className="rounded-lg p-4 border border-border-button bg-bg-input">
<pre className="text-sm">
<code>
{typeof content === 'string'
? content
: JSON.stringify(content, null, 2)}
</code>
</pre>
</div>
);
}
return content;
}, [content]);
return contentElement;
}
export default ServiceDetail;

View File

@ -0,0 +1,460 @@
import { useEffect, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
LucideClipboardList,
LucideDot,
LucideFilter,
LucideSearch,
LucideSettings2,
} from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { cn } from '@/lib/utils';
import { TableEmpty } from '@/components/table-skeleton';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
listServices,
showServiceDetails,
type AdminService,
} from '@/services/admin-service';
import {
EMPTY_DATA,
createColumnFilterFn,
createFuzzySearchFn,
getColumnFilter,
getSortIcon,
setColumnFilter,
} from './utils';
import ServiceDetail from './service-detail';
const columnHelper = createColumnHelper<AdminService.ListServicesItem>();
const globalFilterFn = createFuzzySearchFn<AdminService.ListServicesItem>([
'name',
'service_type',
]);
const SERVICE_TYPE_FILTER_OPTIONS = [
{ value: 'ragflow_server', label: 'ragflow_server' },
{ value: 'meta_data', label: 'meta_data' },
{ value: 'file_store', label: 'file_store' },
{ value: 'retrieval', label: 'retrieval' },
{ value: 'message_queue', label: 'message_queue' },
];
function AdminServiceStatus() {
const { t } = useTranslation();
const [extraInfoModalOpen, setExtraInfoModalOpen] = useState(false);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [itemToMakeAction, setItemToMakeAction] =
useState<AdminService.ListServicesItem | null>(null);
const { data: servicesList, isPending } = useQuery({
queryKey: ['admin/listServices'],
queryFn: async () => (await listServices()).data.data,
});
const {
data: serviceDetails,
isPending: isServiceDetailsPending,
error: serviceDetailsError,
} = useQuery({
queryKey: ['admin/serviceDetails', itemToMakeAction?.id],
queryFn: async () =>
(await showServiceDetails(itemToMakeAction?.id!)).data.data,
enabled: !!(itemToMakeAction && detailModalOpen),
retry: false,
refetchInterval: Infinity,
});
const columnDefs = useMemo(
() => [
columnHelper.accessor('id', {
header: t('admin.id'),
}),
columnHelper.accessor('name', {
header: t('admin.name'),
}),
columnHelper.accessor('service_type', {
header: t('admin.serviceType'),
filterFn: createColumnFilterFn(
(row, id, filterValue) => row.getValue(id) === filterValue,
{
autoRemove: (v) => !v,
resolveFilterValue: (v) => v || null,
},
),
enableSorting: false,
}),
columnHelper.accessor('host', {
header: t('admin.host'),
cell: ({ row }) => (
<Badge variant="secondary" className="font-normal text-text-primary">
<i>{row.getValue('host')}</i>
</Badge>
),
}),
columnHelper.accessor('port', {
header: t('admin.port'),
cell: ({ row }) => (
<Badge variant="secondary" className="font-normal text-text-primary">
<i>{row.getValue('port')}</i>
</Badge>
),
}),
columnHelper.accessor('status', {
header: t('admin.status'),
cell: ({ cell }) => (
<Badge
variant="secondary"
className={cn(
'pl-2 font-normal text-sm text-text-primary capitalize',
{
alive: 'bg-state-success-5 text-state-success',
timeout: 'bg-state-error-5 text-state-error',
fail: 'bg-gray-500/5 text-text-disable',
}[cell.getValue<string>()],
)}
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{cell.getValue()}
</Badge>
),
enableSorting: false,
}),
columnHelper.display({
id: 'actions',
header: t('admin.actions'),
cell: ({ row }) => (
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setItemToMakeAction(row.original);
setExtraInfoModalOpen(true);
}}
>
<LucideSettings2 />
</Button>
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setItemToMakeAction(row.original);
setDetailModalOpen(true);
}}
>
<LucideClipboardList />
</Button>
</div>
),
}),
],
[],
);
const table = useReactTable({
data: servicesList ?? EMPTY_DATA,
columns: columnDefs,
globalFilterFn,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
useEffect(() => {
if (detailModalOpen && serviceDetailsError) {
setDetailModalOpen(false);
}
}, [detailModalOpen, serviceDetailsError]);
return (
<>
<Card className="h-full border border-border-button bg-transparent rounded-xl">
<ScrollArea className="size-full">
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
<CardTitle>{t('admin.serviceStatus')}</CardTitle>
<div className="flex items-center gap-4">
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
>
<LucideFilter className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="bg-bg-base text-text-secondary"
>
<div className="p-2 space-y-6">
<section>
<div className="font-bold mb-3">
{t('admin.serviceType')}
</div>
<RadioGroup
value={
(getColumnFilter(table, 'service_type')
?.value as string) ?? ''
}
onValueChange={(value) =>
setColumnFilter(table, 'service_type', value)
}
>
<Label className="space-x-2">
<RadioGroupItem
className="bg-bg-input border-border-button"
value=""
/>
<span>{t('admin.all')}</span>
</Label>
{SERVICE_TYPE_FILTER_OPTIONS.map(({ label, value }) => (
<Label key={value} className="space-x-2">
<RadioGroupItem
className="bg-bg-input border-border-button"
value={value}
/>
<span>{label}</span>
</Label>
))}
</RadioGroup>
</section>
</div>
<div className="pt-4 flex justify-end">
<Button
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
onClick={() => table.resetColumnFilters()}
>
{t('admin.reset')}
</Button>
</div>
</PopoverContent>
</Popover>
<div className="relative w-56">
<LucideSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
className="pl-10 h-10 bg-bg-input border-border-button"
placeholder={t('header.search')}
value={table.getState().globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<colgroup>
<col className="w-[6%]" />
<col />
<col className="w-[22%]" />
<col className="w-[13%]" />
<col className="w-[10%]" />
<col className="w-[10%]" />
<col className="w-52" />
</colgroup>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<Button
variant="ghost"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{getSortIcon(header.column.getIsSorted())}
</Button>
) : (
flexRender(
header.column.columnDef.header,
header.getContext(),
)
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="group">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableEmpty columnsLength={columnDefs.length} />
)}
</TableBody>
</Table>
</CardContent>
<CardFooter className="flex items-center justify-end">
<RAGFlowPagination
total={servicesList?.length}
current={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
onChange={(page, pageSize) => {
table.setPagination({
pageIndex: page - 1,
pageSize,
});
}}
/>
</CardFooter>
</ScrollArea>
</Card>
{/* Extra info modal*/}
<Dialog open={extraInfoModalOpen} onOpenChange={setExtraInfoModalOpen}>
<DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => {
if (!extraInfoModalOpen) {
setItemToMakeAction(null);
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.extraInfo')}</DialogTitle>
</DialogHeader>
<section className="px-12 pt-6 pb-4">
<div className="rounded-lg p-4 border border-border-button bg-bg-input">
<pre className="text-sm">
<code>
{JSON.stringify(itemToMakeAction?.extra ?? {}, null, 2)}
</code>
</pre>
</div>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => setExtraInfoModalOpen(false)}
>
{t('admin.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Service details modal */}
<Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
<DialogContent
className="flex flex-col max-h-[calc(100vh-4rem)] max-w-6xl p-0 border-border-button"
onAnimationEnd={() => {
if (!detailModalOpen) {
setItemToMakeAction(null);
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>
<Trans i18nKey="admin.serviceDetail">
{{ name: itemToMakeAction?.name }}
</Trans>
</DialogTitle>
</DialogHeader>
<ScrollArea className="pt-6 pb-4 px-12 h-0 flex-1 text-text-secondary flex flex-col">
<ServiceDetail content={serviceDetails?.message} />
</ScrollArea>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => {
setDetailModalOpen(false);
}}
>
{t('admin.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default AdminServiceStatus;

View File

@ -0,0 +1,433 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'umi';
import { LucideArrowLeft, LucideDot, LucideUser2 } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { Avatar } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs-underlined';
import {
getUserDetails,
listUserAgents,
listUserDatasets,
type AdminService,
} from '@/services/admin-service';
import EnterpriseFeature from './components/enterprise-feature';
import { getSortIcon, parseBooleanish } from './utils';
const ASSET_NAMES = ['dataset', 'flow'];
const datasetColumnHelper =
createColumnHelper<AdminService.ListUserDatasetItem>();
const agentColumnHelper = createColumnHelper<AdminService.ListUserAgentItem>();
function UserDatasetTable(props: {
data?: AdminService.ListUserDatasetItem[];
}) {
const { t } = useTranslation();
const columnDefs = useMemo(
() => [
datasetColumnHelper.accessor('name', {
header: t('admin.name'),
enableSorting: false,
}),
datasetColumnHelper.accessor('status', {
header: t('admin.status'),
cell: ({ cell }) => {
return (
<Badge
variant="secondary"
className={cn(
'font-normal text-sm pl-2',
parseBooleanish(cell.getValue())
? 'bg-state-success-5 text-state-success'
: 'bg-state-error-5 text-state-error',
)}
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(
parseBooleanish(cell.getValue())
? 'admin.active'
: 'admin.inactive',
)}
</Badge>
);
},
enableSorting: false,
}),
datasetColumnHelper.accessor('chunk_num', {
header: t('admin.chunkNum'),
}),
datasetColumnHelper.accessor('doc_num', {
header: t('admin.docNum'),
}),
datasetColumnHelper.accessor('token_num', {
header: t('admin.tokenNum'),
}),
datasetColumnHelper.accessor('language', {
header: t('admin.language'),
enableSorting: false,
}),
datasetColumnHelper.accessor('create_date', {
header: t('admin.createDate'),
}),
datasetColumnHelper.accessor('update_date', {
header: t('admin.updateDate'),
}),
datasetColumnHelper.accessor('permission', {
header: t('admin.permission'),
enableSorting: false,
}),
],
[t],
);
const table = useReactTable({
data: props.data ?? [],
columns: columnDefs,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<section className="space-y-4">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : header.column.getCanSort() ? (
<Button
variant="ghost"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{getSortIcon(header.column.getIsSorted())}
</Button>
) : (
flexRender(
header.column.columnDef.header,
header.getContext(),
)
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
>
{t('common.noData')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<RAGFlowPagination
total={props.data?.length}
current={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
onChange={(page, pageSize) => {
table.setPagination({
pageIndex: page - 1,
pageSize,
});
}}
/>
</section>
);
}
function UserAgentTable(props: { data?: AdminService.ListUserAgentItem[] }) {
const { t } = useTranslation();
const columnDefs = useMemo(
() => [
agentColumnHelper.accessor('title', {
header: t('admin.agentTitle'),
}),
agentColumnHelper.accessor('permission', {
header: t('admin.permission'),
}),
agentColumnHelper.accessor('canvas_category', {
header: t('admin.canvasCategory'),
}),
],
[t],
);
const table = useReactTable({
data: props.data ?? [],
columns: columnDefs,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<section className="space-y-4">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : (
<>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{/* {header.column.getCanFilter() && (
<Button
variant="ghost"
>
<LucideFilter />
</Button>
)} */}
</>
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow key="empty">
<TableCell
colSpan={table.getAllColumns().length}
className="h-24 text-center"
>
{t('common.noData')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<RAGFlowPagination
total={props.data?.length}
current={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
onChange={(page, pageSize) => {
table.setPagination({
pageIndex: page - 1,
pageSize,
});
}}
/>
</section>
);
}
function AdminUserDetail() {
const navigate = useNavigate();
const { t } = useTranslation();
const { id } = useParams();
const { data: { detail, datasets, agents } = {} } = useQuery({
queryKey: ['admin/userDetail', id],
queryFn: async () => {
const [userDetails, userDatasets, userAgents] = await Promise.all([
getUserDetails(id!),
listUserDatasets(id!),
listUserAgents(id!),
]);
return {
detail: userDetails.data.data[0],
datasets: userDatasets.data.data,
agents: userAgents.data.data,
};
},
enabled: !!id,
});
return (
<section className="px-10 py-5 size-full flex flex-col">
<nav className="mb-5">
<Button
variant="outline"
className="h-10 px-3 dark:bg-bg-input dark:border-border-button"
onClick={() => navigate(`${Routes.AdminUserManagement}`)}
>
<LucideArrowLeft />
<span>{t('admin.userManagement')}</span>
</Button>
</nav>
<Card className="h-0 basis-0 grow flex flex-col bg-transparent border dark:border-border-button overflow-hidden">
<CardHeader className="pb-10 border-b dark:border-border-button space-y-8">
<section className="flex items-center gap-4 text-base">
<Avatar className="justify-center items-center bg-bg-group uppercase">
{detail?.email
.split('@')[0]
.replace(/[^0-9a-z]/gi, '')
.slice(0, 2) || <LucideUser2 />}
</Avatar>
<span>{detail?.email}</span>
<Badge
variant="secondary"
className={cn(
'font-normal text-sm pl-2',
parseBooleanish(detail?.is_active)
? 'bg-state-success-5 text-state-success'
: '',
)}
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(
parseBooleanish(detail?.is_active)
? 'admin.active'
: 'admin.inactive',
)}
</Badge>
<EnterpriseFeature>
{() => (
<Badge variant="secondary" className="font-normal text-sm">
{detail?.role}
</Badge>
)}
</EnterpriseFeature>
</section>
<section className="flex items-start px-14 space-x-14">
<div>
<div className="text-sm text-text-secondary mb-2">
{t('admin.lastLoginTime')}
</div>
<div>{detail?.last_login_time}</div>
</div>
<div>
<div className="text-sm text-text-secondary mb-2">
{t('admin.createTime')}
</div>
<div>{detail?.create_date}</div>
</div>
<div>
<div className="text-sm text-text-secondary mb-2">
{t('admin.lastUpdateTime')}
</div>
<div>{detail?.update_date}</div>
</div>
<div>
<div className="text-sm text-text-secondary mb-2">
{t('admin.language')}
</div>
<div>{detail?.language}</div>
</div>
<div>
<div className="text-sm text-text-secondary mb-2">
{t('admin.isAnonymous')}
</div>
<div>{t(detail?.is_anonymous ? 'admin.yes' : 'admin.no')}</div>
</div>
</section>
</CardHeader>
<CardContent className="h-0 basis-0 grow pt-6">
<Tabs className="h-full flex flex-col" defaultValue="dataset">
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
{ASSET_NAMES.map((name) => (
<TabsTrigger
key={name}
className="border-border-button data-[state=active]:bg-bg-card"
value={name}
>
{t(`header.${name}`)}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="dataset" className="h-0 basis-0 grow">
<ScrollArea className="h-full">
<UserDatasetTable data={datasets} />
</ScrollArea>
</TabsContent>
<TabsContent value="flow" className="h-0 basis-0 grow">
<ScrollArea className="h-full">
<UserAgentTable data={agents} />
</ScrollArea>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</section>
);
}
export default AdminUserDetail;

View File

@ -0,0 +1,691 @@
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
LucideClipboardList,
LucideDot,
LucideTrash2,
LucideUserLock,
LucideUserPlus,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { rsaPsw } from '@/utils';
import { TableEmpty } from '@/components/table-skeleton';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { LoadingButton } from '@/components/ui/loading-button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Routes } from '@/routes';
import { LucideFilter, LucideSearch } from 'lucide-react';
import useChangePasswordForm from './forms/change-password-form';
import useCreateUserForm from './forms/user-form';
import {
createUser,
deleteUser,
listRoles,
listUsers,
updateUserPassword,
updateUserRole,
updateUserStatus,
type AdminService,
} from '@/services/admin-service';
import {
createColumnFilterFn,
createFuzzySearchFn,
EMPTY_DATA,
IS_ENTERPRISE,
parseBooleanish,
} from './utils';
import EnterpriseFeature from './components/enterprise-feature';
const columnHelper = createColumnHelper<AdminService.ListUsersItem>();
const globalFilterFn = createFuzzySearchFn<AdminService.ListUsersItem>([
'email',
'nickname',
]);
const STATUS_FILTER_OPTIONS = [
{ value: '', label: 'admin.all' },
{ value: 'active', label: 'admin.active' },
{ value: 'inactive', label: 'admin.inactive' },
];
function AdminUserManagement() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
const [createUserModalOpen, setCreateUserModalOpen] = useState(false);
const [userToMakeAction, setUserToMakeAction] =
useState<AdminService.ListUsersItem | null>(null);
const changePasswordForm = useChangePasswordForm();
const createUserForm = useCreateUserForm();
const { data: roleList } = useQuery({
queryKey: ['admin/listRoles'],
queryFn: async () => (await listRoles()).data.data.roles,
});
const { data: usersList, isPending } = useQuery({
queryKey: ['admin/listUsers'],
queryFn: async () => (await listUsers()).data.data,
});
// Delete user mutation
const deleteUserMutation = useMutation({
mutationFn: deleteUser,
onSuccess: () => {
// message.success(t('admin.userDeletedSuccessfully'));
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
setDeleteModalOpen(false);
setUserToMakeAction(null);
},
});
// Change password mutation
const changePasswordMutation = useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
updateUserPassword(email, password),
onSuccess: () => {
// message.success(t('admin.passwordChangedSuccessfully'));
setPasswordModalOpen(false);
setUserToMakeAction(null);
},
});
// Update user role mutation
const updateUserRoleMutation = useMutation({
mutationFn: ({ email, role }: { email: string; role: string }) =>
updateUserRole(email, role),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
},
});
// Create user mutation
const createUserMutation = useMutation({
mutationFn: async ({
email,
password,
role,
}: {
email: string;
password: string;
role?: string;
}) => {
await createUser(email, password);
if (IS_ENTERPRISE && role) {
await updateUserRoleMutation.mutateAsync({ email, role });
}
},
onSuccess: () => {
// message.success(t('admin.userCreatedSuccessfully'));
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
setCreateUserModalOpen(false);
createUserForm.form.reset();
},
});
// Update user status mutation
const updateUserStatusMutation = useMutation({
mutationFn: (data: { email: string; isActive: boolean }) =>
updateUserStatus(data.email, data.isActive ? 'on' : 'off'),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
},
});
const columnDefs = useMemo(
() => [
columnHelper.accessor('email', {
header: t('admin.email'),
}),
columnHelper.accessor('nickname', {
header: t('admin.nickname'),
}),
...(IS_ENTERPRISE
? [
columnHelper.accessor('role', {
header: t('admin.role'),
cell: ({ row, cell }) => (
<Select
value={cell.getValue()}
onValueChange={(value) => {
if (!updateUserRoleMutation.isPending) {
updateUserRoleMutation.mutate({
email: row.original.email,
role: value,
});
}
}}
disabled={updateUserRoleMutation.isPending}
>
<SelectTrigger className="h-10">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-bg-base">
{roleList?.map(({ id, role_name }) => (
<SelectItem key={id} value={role_name}>
{role_name}
</SelectItem>
))}
</SelectContent>
</Select>
),
filterFn: createColumnFilterFn(
(row, id, filterValue) => row.getValue(id) === filterValue,
{
autoRemove: (v) => !v,
},
),
}),
]
: []),
columnHelper.display({
id: 'enable',
header: t('admin.enable'),
cell: ({ row }) => (
<Switch
checked={parseBooleanish(row.original.is_active)}
onCheckedChange={(checked) => {
updateUserStatusMutation.mutate({
email: row.original.email,
isActive: checked,
});
}}
disabled={updateUserStatusMutation.isPending}
/>
),
}),
columnHelper.accessor('is_active', {
header: t('admin.status'),
cell: ({ cell }) => (
<Badge
variant="secondary"
className={cn(
'pl-2 font-normal text-sm',
parseBooleanish(cell.getValue())
? 'bg-state-success-5 text-state-success'
: '',
)}
>
<LucideDot className="size-[1em] stroke-[8] mr-1" />
{t(
parseBooleanish(cell.getValue())
? 'admin.active'
: 'admin.inactive',
)}
</Badge>
),
filterFn: createColumnFilterFn(
(row, id, filterValue) => row.getValue(id) === filterValue,
{
autoRemove: (v) => !v,
resolveFilterValue: (v) =>
v ? (v === 'active' ? '1' : '0') : null,
},
),
}),
columnHelper.display({
id: 'actions',
header: t('admin.actions'),
cell: ({ row }) => (
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() =>
navigate(`${Routes.AdminUserManagement}/${row.original.email}`)
}
>
<LucideClipboardList />
</Button>
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setUserToMakeAction(row.original);
setPasswordModalOpen(true);
}}
>
<LucideUserLock />
</Button>
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setUserToMakeAction(row.original);
setDeleteModalOpen(true);
}}
>
<LucideTrash2 />
</Button>
</div>
),
}),
],
[
roleList,
t,
navigate,
updateUserStatusMutation.isPending,
updateUserRoleMutation.isPending,
],
);
const table = useReactTable({
data: usersList ?? EMPTY_DATA,
columns: columnDefs,
globalFilterFn,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<>
<Card className="h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<ScrollArea className="size-full">
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
<CardTitle>{t('admin.userManagement')}</CardTitle>
<div className="ml-auto flex justify-end gap-4">
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
>
<LucideFilter className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="bg-bg-base text-text-secondary"
>
<div className="p-2 space-y-6">
<EnterpriseFeature>
{() => (
<section>
<div className="font-bold mb-3">
{t('admin.role')}
</div>
<RadioGroup
value={
(table
.getColumn('role')
?.getFilterValue() as string) ?? ''
}
onValueChange={(value) =>
table.getColumn('role')?.setFilterValue(value)
}
>
<Label className="space-x-2">
<RadioGroupItem value="" />
<span>{t('admin.all')}</span>
</Label>
{roleList?.map(({ id, role_name }) => (
<Label key={id} className="space-x-2">
<RadioGroupItem
className="bg-bg-input border-border-button"
value={role_name}
/>
<span>{role_name}</span>
</Label>
))}
</RadioGroup>
</section>
)}
</EnterpriseFeature>
<section>
<div className="font-bold mb-3">{t('admin.status')}</div>
<RadioGroup
value={
(table
.getColumn('is_active')
?.getFilterValue() as string) ?? ''
}
onValueChange={(value) =>
table.getColumn('is_active')?.setFilterValue(value)
}
>
{STATUS_FILTER_OPTIONS.map(({ label, value }) => (
<Label key={value} className="space-x-2">
<RadioGroupItem
className="bg-bg-input border-border-button"
value={value}
/>
<span>{t(label)}</span>
</Label>
))}
</RadioGroup>
</section>
</div>
<div className="pt-4 flex justify-end">
<Button
variant="outline"
className="dark:bg-bg-input dark:border-border-button text-text-secondary"
onClick={() => table.resetColumnFilters()}
>
{t('admin.reset')}
</Button>
</div>
</PopoverContent>
</Popover>
<div className="relative w-56">
<LucideSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
className="pl-10 h-10 bg-bg-input border-border-button"
placeholder={t('header.search')}
value={table.getState().globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
</div>
<Button
className="h-10 px-4"
onClick={() => setCreateUserModalOpen(true)}
>
<LucideUserPlus />
{t('admin.newUser')}
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<colgroup>
<col width="*" />
<col className="w-[22%]" />
<EnterpriseFeature>
{() => <col className="w-[12%]" />}
</EnterpriseFeature>
<col className="w-[10%]" />
<col className="w-[12%]" />
<col className="w-52" />
</colgroup>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="group">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableEmpty key="empty" columnsLength={columnDefs.length} />
)}
</TableBody>
</Table>
</CardContent>
<CardFooter className="flex items-center justify-end">
<RAGFlowPagination
total={usersList?.length ?? 0}
current={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
onChange={(page, pageSize) => {
table.setPagination({
pageIndex: page - 1,
pageSize,
});
}}
/>
</CardFooter>
</ScrollArea>
</Card>
{/* Delete Confirmation Modal */}
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<DialogContent className="p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.deleteUser')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4">
<DialogDescription className="text-text-primary">
{t('admin.deleteUserConfirmation')}
<div className="rounded-lg mt-6 p-4 border border-border-button">
{userToMakeAction?.email}
</div>
</DialogDescription>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => setDeleteModalOpen(false)}
disabled={deleteUserMutation.isPending}
>
{t('admin.cancel')}
</Button>
<LoadingButton
className="px-4 h-10"
variant="destructive"
onClick={() =>
deleteUserMutation.mutate(userToMakeAction?.email || '')
}
disabled={deleteUserMutation.isPending}
loading={deleteUserMutation.isPending}
>
{t('admin.delete')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Change Password Modal */}
<Dialog open={passwordModalOpen} onOpenChange={setPasswordModalOpen}>
<DialogContent className="p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.changePassword')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4 text-text-secondary">
<changePasswordForm.FormComponent
key="changePasswordForm"
email={userToMakeAction?.email || ''}
onSubmit={({ newPassword }) => {
if (userToMakeAction) {
changePasswordMutation.mutate({
email: userToMakeAction.email,
password: rsaPsw(newPassword) as string,
});
}
}}
/>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => {
setPasswordModalOpen(false);
setUserToMakeAction(null);
}}
disabled={changePasswordMutation.isPending}
>
{t('admin.cancel')}
</Button>
<LoadingButton
form={changePasswordForm.id}
className="px-4 h-10"
variant="default"
type="submit"
disabled={changePasswordMutation.isPending}
loading={changePasswordMutation.isPending}
>
{t('admin.changePassword')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Create User Modal */}
<Dialog
open={createUserModalOpen}
onOpenChange={() => {
setCreateUserModalOpen(false);
createUserForm.form.reset();
}}
>
<DialogContent className="p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.createNewUser')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4">
<createUserForm.FormComponent
id={createUserForm.id}
onSubmit={({ email, password }) => {
createUserMutation.mutate({
email: email,
password: rsaPsw(password) as string,
});
}}
/>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => {
setCreateUserModalOpen(false);
createUserForm.form.reset();
}}
disabled={createUserMutation.isPending}
>
{t('admin.cancel')}
</Button>
<LoadingButton
form={createUserForm.id}
type="submit"
className="px-4 h-10"
variant="default"
disabled={createUserMutation.isPending}
loading={createUserMutation.isPending}
>
{t('admin.confirm')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default AdminUserManagement;

View File

@ -0,0 +1,85 @@
import {
ColumnFilterAutoRemoveTestFn,
FilterFn,
Row,
RowData,
SortDirection,
Table,
TransformFilterValueFn,
} from '@tanstack/react-table';
import { LucideSortAsc, LucideSortDesc } from 'lucide-react';
export function parseBooleanish(value: any): boolean {
return typeof value === 'string'
? /^(1|[Tt]rue|[Oo]n|[Yy](es)?)$/.test(value)
: !!value;
}
export function createFuzzySearchFn<TData extends RowData>(
columns: (keyof TData)[] = [],
) {
return (row: Row<TData>, columnId: string, filterValue: string) => {
const searchText = filterValue.trim().toLowerCase();
return columns
.map((n) =>
row
.getValue<string>(n as string)
.trim()
.toLowerCase(),
)
.some((v) => v.includes(searchText));
};
}
export function createColumnFilterFn<TData extends RowData>(
filterFn: FilterFn<TData>,
options: {
resolveFilterValue?: TransformFilterValueFn<TData>;
autoRemove?: ColumnFilterAutoRemoveTestFn<TData>;
},
) {
return Object.assign(filterFn, options) as FilterFn<TData>;
}
export function getColumnFilter<TData extends RowData>(
table: Table<TData>,
columnId: string,
) {
return table
.getState()
.columnFilters.find((filter) => filter.id === columnId);
}
export function setColumnFilter<TData extends RowData>(
table: Table<TData>,
columnId: string,
value?: unknown,
) {
const otherColumnFilters = table
.getState()
.columnFilters.filter((filter) => filter.id !== columnId);
if (value == null) {
table.setColumnFilters(otherColumnFilters);
} else {
table.setColumnFilters([
...otherColumnFilters,
{
id: columnId,
value,
},
]);
}
}
export function getSortIcon(sorting: false | SortDirection) {
return {
asc: <LucideSortAsc />,
desc: <LucideSortDesc />,
}[sorting as string];
}
export const EMPTY_DATA = Object.freeze<any[]>([]) as any[];
export const IS_ENTERPRISE =
process.env.UMI_APP_RAGFLOW_ENTERPRISE === 'RAGFLOW_ENTERPRISE';

View File

@ -0,0 +1,522 @@
import { TableEmpty } from '@/components/table-skeleton';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { LoadingButton } from '@/components/ui/loading-button';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
LucideDownload,
LucidePlus,
LucideSearch,
LucideTrash2,
LucideUpload,
LucideUserPen,
} from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import useCreateEmailForm from './forms/email-form';
import useImportExcelForm from './forms/import-excel-form';
import { EMPTY_DATA, createFuzzySearchFn } from './utils';
// #region FAKE DATA
function _pickRandom<T extends unknown>(arr: T[]): T | void {
return arr[Math.floor(Math.random() * arr.length)];
}
const PSEUDO_TABLE_ITEMS = Array.from({ length: 20 }, () => ({
id: Math.random().toString(36).slice(2, 8),
email: `${Math.random().toString(36).slice(2, 6)}@example.com`,
created_by: _pickRandom(['Alice', 'Bob', 'Carol', 'Dave']) || 'System',
created_at: Date.now() - Math.floor(Math.random() * 1000 * 60 * 60 * 24 * 30),
}));
// #endregion
const columnHelper = createColumnHelper<(typeof PSEUDO_TABLE_ITEMS)[0]>();
const globalFilterFn = createFuzzySearchFn<(typeof PSEUDO_TABLE_ITEMS)[0]>([
'email',
]);
function AdminWhitelist() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const createEmailForm = useCreateEmailForm();
const importExcelForm = useImportExcelForm();
const [emailToMakeAction, setEmailToMakeAction] = useState<string | null>(
null,
);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [importModalOpen, setImportModalOpen] = useState(false);
// Reset form when editing a different email
useEffect(() => {
if (emailToMakeAction && editModalOpen) {
createEmailForm.form.setValue('email', emailToMakeAction);
}
}, [emailToMakeAction, editModalOpen, createEmailForm.form]);
const { isPending: isCreating, mutateAsync: createEmail } = useMutation({
mutationFn: async (data: { email: string }) => {
/* create email API call */
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
setCreateModalOpen(false);
setEmailToMakeAction(null);
createEmailForm.form.reset();
},
onError: (error) => {
console.error('Error creating email:', error);
},
});
const { isPending: isEditing, mutateAsync: updateEmail } = useMutation({
mutationFn: async (data: { email: string }) => {
/* update email API call */
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
setEditModalOpen(false);
setEmailToMakeAction(null);
createEmailForm.form.reset();
},
});
const { isPending: isDeleting, mutateAsync: deleteEmail } = useMutation({
mutationFn: async (data: { email: string }) => {
/* delete email API call */
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
setDeleteModalOpen(false);
setEmailToMakeAction(null);
},
onError: (error) => {
console.error('Error deleting email:', error);
},
});
const { isPending: isImporting, mutateAsync: importExcel } = useMutation({
mutationFn: async (data: {
file: FileList;
overwriteExisting: boolean;
}) => {
/* import Excel API call */
console.log(
'Importing Excel file:',
data.file[0]?.name,
'Overwrite:',
data.overwriteExisting,
);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
setImportModalOpen(false);
importExcelForm.form.reset();
},
onError: (error) => {
console.error('Error importing Excel:', error);
},
});
const columnDefs = useMemo(
() => [
columnHelper.accessor('email', {
header: 'Email',
}),
columnHelper.accessor('created_by', {
header: 'Created by',
}),
columnHelper.accessor('created_at', {
header: 'Created date',
cell: ({ row }) =>
new Date(row.getValue('created_at') as number).toLocaleString(),
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: ({ row }) => (
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setEmailToMakeAction(row.original.email);
setEditModalOpen(true);
}}
>
<LucideUserPen />
</Button>
<Button
variant="transparent"
size="icon"
className="border-0 text-text-secondary"
onClick={() => {
setEmailToMakeAction(row.original.email);
setDeleteModalOpen(true);
}}
>
<LucideTrash2 />
</Button>
</div>
),
}),
],
[],
);
const table = useReactTable({
data: PSEUDO_TABLE_ITEMS ?? EMPTY_DATA,
columns: columnDefs,
globalFilterFn,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<>
<Card className="h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
<ScrollArea className="size-full">
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
<CardTitle>{t('admin.whitelistManagement')}</CardTitle>
<div className="flex items-center gap-4">
<div className="relative w-56">
<LucideSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
className="pl-10 h-10 bg-bg-input border-border-button"
placeholder={t('header.search')}
value={table.getState().globalFilter}
onChange={(e) => table.setGlobalFilter(e.target.value)}
/>
</div>
<Button
variant="outline"
className="h-10 px-4 dark:bg-bg-input dark:border-border-button text-text-secondary"
>
<LucideUpload />
{t('admin.exportAsExcel')}
</Button>
<Button
variant="outline"
className="h-10 px-4 dark:bg-bg-input dark:border-border-button text-text-secondary"
onClick={() => setImportModalOpen(true)}
>
<LucideDownload />
{t('admin.importFromExcel')}
</Button>
<Button
className="h-10 px-4"
onClick={() => setCreateModalOpen(true)}
>
<LucidePlus />
{t('admin.createEmail')}
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<colgroup>
<col />
<col className="w-[20%]" />
<col className="w-[30%]" />
<col className="w-[12rem]" />
</colgroup>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="group">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableEmpty columnsLength={columnDefs.length} />
)}
</TableBody>
</Table>
</CardContent>
<CardFooter className="flex items-center justify-end">
<RAGFlowPagination
total={table.getFilteredRowModel().rows.length}
current={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
onChange={(page, pageSize) => {
table.setPagination({
pageIndex: page - 1,
pageSize,
});
}}
/>
</CardFooter>
</ScrollArea>
</Card>
{/* Delete Confirmation Modal */}
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => {
if (!deleteModalOpen) {
setEmailToMakeAction(null);
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.deleteEmail')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4">
<DialogDescription className="text-text-primary">
{t('admin.deleteWhitelistEmailConfirmation')}
<div className="rounded-lg mt-6 p-4 border border-border-button">
{emailToMakeAction}
</div>
</DialogDescription>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => setDeleteModalOpen(false)}
disabled={isDeleting}
>
{t('admin.cancel')}
</Button>
<LoadingButton
className="px-4 h-10"
variant="destructive"
onClick={() => {
deleteEmail({ email: emailToMakeAction! });
}}
disabled={isDeleting}
loading={isDeleting}
>
{t('admin.delete')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Create Email Modal */}
<Dialog
open={createModalOpen}
onOpenChange={() => {
setCreateModalOpen(false);
createEmailForm.form.reset();
}}
>
<DialogContent className="p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.createEmail')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4 text-text-secondary">
<createEmailForm.FormComponent
id={createEmailForm.id}
onSubmit={createEmail}
/>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => {
setCreateModalOpen(false);
createEmailForm.form.reset();
}}
disabled={isCreating}
>
{t('admin.cancel')}
</Button>
<LoadingButton
form={createEmailForm.id}
type="submit"
className="px-4 h-10"
variant="default"
disabled={isCreating}
loading={isCreating}
>
{t('admin.confirm')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Email Modal */}
<Dialog
open={editModalOpen}
onOpenChange={() => {
setEditModalOpen(false);
setEmailToMakeAction(null);
createEmailForm.form.reset();
}}
>
<DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => {
if (!editModalOpen) {
setEmailToMakeAction(null);
createEmailForm.form.reset();
}
}}
>
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.editEmail')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4 text-text-secondary">
<createEmailForm.FormComponent
id={createEmailForm.id}
onSubmit={updateEmail}
/>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => {
setEditModalOpen(false);
setEmailToMakeAction(null);
createEmailForm.form.reset();
}}
disabled={isEditing}
>
{t('admin.cancel')}
</Button>
<LoadingButton
form={createEmailForm.id}
type="submit"
className="px-4 h-10"
variant="default"
disabled={isEditing}
loading={isEditing}
>
{t('admin.confirm')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Import Excel Modal */}
<Dialog open={importModalOpen} onOpenChange={setImportModalOpen}>
<DialogContent className="p-0 border-border-button">
<DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.importWhitelist')}</DialogTitle>
</DialogHeader>
<section className="px-12 py-4 text-text-secondary">
<importExcelForm.FormComponent
id={importExcelForm.id}
onSubmit={importExcel}
/>
</section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
<Button
className="px-4 h-10 dark:border-border-button"
variant="outline"
onClick={() => {
setImportModalOpen(false);
importExcelForm.form.reset();
}}
disabled={isImporting}
>
{t('admin.cancel')}
</Button>
<LoadingButton
form={importExcelForm.id}
type="submit"
className="px-4 h-10"
variant="default"
disabled={isImporting}
loading={isImporting}
>
{t('admin.import')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export default AdminWhitelist;

View File

@ -1,3 +1,5 @@
import { IS_ENTERPRISE } from './pages/admin/utils';
export enum Routes {
Root = '/',
Login = '/login-next',
@ -47,6 +49,12 @@ export enum Routes {
DataSetOverview = '/dataset-overview',
DataSetSetting = '/dataset-setting',
DataflowResult = '/dataflow-result',
Admin = '/admin',
AdminServices = `${Admin}/services`,
AdminUserManagement = `${Admin}/users`,
AdminWhitelist = `${Admin}/whitelist`,
AdminRoles = `${Admin}/roles`,
AdminMonitoring = `${Admin}/monitoring`,
}
const routes = [
@ -394,6 +402,56 @@ const routes = [
},
],
},
// Admin routes
{
path: Routes.Admin,
component: `@/pages/admin`,
layout: false,
},
{
path: `${Routes.AdminUserManagement}/:id`,
layout: false,
wrappers: ['@/wrappers/authAdmin'],
component: `@/pages/admin/user-detail`,
},
{
path: Routes.Admin,
component: `@/pages/admin/layout`,
layout: false,
routes: [
{
path: Routes.AdminServices,
component: `@/pages/admin/service-status`,
wrappers: ['@/wrappers/authAdmin'],
},
{
path: Routes.AdminUserManagement,
component: `@/pages/admin/users`,
wrappers: ['@/wrappers/authAdmin'],
},
...(IS_ENTERPRISE
? [
{
path: Routes.AdminWhitelist,
component: `@/pages/admin/whitelist`,
wrappers: ['@/wrappers/authAdmin'],
},
{
path: Routes.AdminRoles,
component: `@/pages/admin/roles`,
wrappers: ['@/wrappers/authAdmin'],
},
{
path: Routes.AdminMonitoring,
component: `@/pages/admin/monitoring`,
wrappers: ['@/wrappers/authAdmin'],
},
]
: []),
],
},
];
export default routes;

View File

@ -0,0 +1,379 @@
import { message, notification } from 'antd';
import axios from 'axios';
import { Navigate } from 'umi';
import { Authorization } from '@/constants/authorization';
import i18n from '@/locales/config';
import { Routes } from '@/routes';
import api from '@/utils/api';
import authorizationUtil, {
getAuthorization,
} from '@/utils/authorization-util';
import { convertTheKeysOfTheObjectToSnake } from '@/utils/common-util';
import { ResultCode, RetcodeMessage } from '@/utils/request';
const request = axios.create({
timeout: 300000,
});
request.interceptors.request.use((config) => {
const data = convertTheKeysOfTheObjectToSnake(config.data);
const params = convertTheKeysOfTheObjectToSnake(config.params) as any;
const newConfig = { ...config, data, params };
// @ts-ignore
if (!newConfig.skipToken) {
newConfig.headers.set(Authorization, getAuthorization());
}
return newConfig;
});
request.interceptors.response.use(
(response) => {
if (response.config.responseType === 'blob') {
return response;
}
const { data } = response ?? {};
if (data?.code === 100) {
message.error(data?.message);
} else if (data?.code === 401) {
notification.error({
message: data?.message,
description: data?.message,
duration: 3,
});
authorizationUtil.removeAll();
Navigate({ to: Routes.Admin });
} else if (data?.code && data.code !== 0) {
notification.error({
message: `${i18n.t('message.hint')}: ${data?.code}`,
description: data?.message,
duration: 3,
});
}
return response;
},
(error) => {
const { response, message } = error;
const { data } = response ?? {};
if (error.message === 'Failed to fetch') {
notification.error({
description: i18n.t('message.networkAnomalyDescription'),
message: i18n.t('message.networkAnomaly'),
});
} else if (data?.code === 100) {
message.error(data?.message);
} else if (data?.code === 401) {
notification.error({
message: data?.message,
description: data?.message,
duration: 3,
});
authorizationUtil.removeAll();
Navigate({ to: Routes.Admin });
} else if (data?.code && data.code !== 0) {
notification.error({
message: `${i18n.t('message.hint')}: ${data?.code}`,
description: data?.message,
duration: 3,
});
} else if (response.status) {
notification.error({
message: `${i18n.t('message.requestError')} ${response.status}: ${response.config.url}`,
description:
RetcodeMessage[response.status as ResultCode] || response.statusText,
});
} else if (response.status === 413 || response?.status === 504) {
message.error(RetcodeMessage[response?.status as ResultCode]);
} else if (response.status === 401) {
notification.error({
message: response.data.message,
description: response.data.message,
duration: 3,
});
authorizationUtil.removeAll();
window.location.href = location.origin + '/admin';
}
return error;
},
);
const {
adminLogin,
adminLogout,
adminListUsers,
adminCreateUser,
adminGetUserDetails: adminShowUserDetails,
adminUpdateUserStatus,
adminUpdateUserPassword,
adminDeleteUser,
adminListUserDatasets,
adminListUserAgents,
adminListServices,
adminShowServiceDetails,
adminListRoles,
adminListRolesWithPermission,
adminCreateRole,
adminDeleteRole,
adminUpdateRoleDescription,
adminGetRolePermissions,
adminAssignRolePermissions,
adminRevokeRolePermissions,
adminGetUserPermissions,
adminUpdateUserRole,
adminListResources,
} = api;
type ResponseData<D = {}> = {
code: number;
message: string;
data: D;
};
export namespace AdminService {
export type LoginData = {
access_token: string;
avatar: unknown;
color_schema: 'Bright' | 'Dark';
create_date: string;
create_time: number;
email: string;
id: string;
is_active: '0' | '1';
is_anonymous: '0' | '1';
is_authenticated: '0' | '1';
is_superuser: boolean;
language: string;
last_login_time: string;
login_channel: unknown;
nickname: string;
password: string;
status: '0' | '1';
timezone: string;
update_date: [string];
update_time: [number];
};
export type ListUsersItem = {
create_date: string;
email: string;
is_active: '0' | '1';
is_superuser: boolean;
role: string;
nickname: string;
};
export type UserDetail = {
create_date: string;
email: string;
is_active: '0' | '1';
is_anonymous: '0' | '1';
is_superuser: boolean;
language: string;
last_login_time: string;
login_channel: unknown;
status: '0' | '1';
update_date: string;
role: string;
};
export type ListUserDatasetItem = {
chunk_num: number;
create_date: string;
doc_num: number;
language: string;
name: string;
permission: string;
status: '0' | '1';
token_num: number;
update_date: string;
};
export type ListUserAgentItem = {
canvas_category: 'agent';
permission: 'string';
title: string;
};
export type ListServicesItem = {
extra: Record<string, unknown>;
host: string;
id: number;
name: string;
port: number;
service_type: string;
status: 'alive' | 'timeout' | 'fail';
};
export type ServiceDetail = {
service_name: string;
status: 'alive' | 'timeout';
message: string | Record<string, any> | Record<string, any>[];
};
export type PermissionData = {
enable: boolean;
read: boolean;
write: boolean;
share: boolean;
};
export type ListRoleItem = {
id: string;
role_name: string;
description: string;
create_date: string;
update_date: string;
};
export type ListRoleItemWithPermission = ListRoleItem & {
permissions: Record<string, PermissionData>;
};
export type RoleDetailWithPermission = {
role: {
id: string;
name: string;
description: string;
};
permissions: Record<string, PermissionData>;
};
export type RoleDetail = {
id: string;
name: string;
descrtiption: string;
create_date: string;
update_date: string;
};
export type AssignRolePermissionInput = {
permissions: Record<string, Partial<PermissionData>>;
};
export type RevokeRolePermissionInput = AssignRolePermissionInput;
export type UserDetailWithPermission = {
user: {
id: string;
username: string;
role: string;
};
role_permissions: Record<string, PermissionData>;
};
export type ResourceType = {
resource_types: string[];
};
}
export const login = (params: { email: string; password: string }) =>
request.post<ResponseData<AdminService.LoginData>>(adminLogin, params);
export const logout = () => request.get<ResponseData<boolean>>(adminLogout);
export const listUsers = () =>
request.get<ResponseData<AdminService.ListUsersItem[]>>(adminListUsers, {});
export const createUser = (email: string, password: string) =>
request.post<ResponseData<boolean>>(adminCreateUser, {
username: email,
password,
});
export const getUserDetails = (email: string) =>
request.get<ResponseData<[AdminService.UserDetail]>>(
adminShowUserDetails(email),
);
export const listUserDatasets = (email: string) =>
request.get<ResponseData<AdminService.ListUserDatasetItem[]>>(
adminListUserDatasets(email),
);
export const listUserAgents = (email: string) =>
request.get<ResponseData<AdminService.ListUserAgentItem[]>>(
adminListUserAgents(email),
);
export const updateUserStatus = (email: string, status: 'on' | 'off') =>
request.put(adminUpdateUserStatus(email), { activate_status: status });
export const updateUserPassword = (email: string, password: string) =>
request.put(adminUpdateUserPassword(email), { new_password: password });
export const deleteUser = (email: string) =>
request.delete(adminDeleteUser(email));
export const listServices = () =>
request.get<ResponseData<AdminService.ListServicesItem[]>>(adminListServices);
export const showServiceDetails = (serviceId: number) =>
request.get<ResponseData<AdminService.ServiceDetail>>(
adminShowServiceDetails(String(serviceId)),
);
export const createRole = (params: { roleName: string; description: string }) =>
request.post<ResponseData<AdminService.RoleDetail>>(adminCreateRole, params);
export const updateRoleDescription = (role: string, description: string) =>
request.put<ResponseData<AdminService.RoleDetail>>(
adminUpdateRoleDescription(role),
{ description },
);
export const deleteRole = (role: string) =>
request.delete<ResponseData<ResponseData<never>>>(adminDeleteRole(role));
export const listRoles = () =>
request.get<
ResponseData<{ roles: AdminService.ListRoleItem[]; total: number }>
>(adminListRoles);
export const listRolesWithPermission = () =>
request.get<
ResponseData<{
roles: AdminService.ListRoleItemWithPermission[];
total: number;
}>
>(adminListRolesWithPermission);
export const getRolePermissions = (role: string) =>
request.get<ResponseData<AdminService.RoleDetailWithPermission>>(
adminGetRolePermissions(role),
);
export const assignRolePermissions = (
role: string,
params: AdminService.AssignRolePermissionInput,
) =>
request.post<ResponseData<never>>(adminAssignRolePermissions(role), params);
export const revokeRolePermissions = (
role: string,
params: AdminService.RevokeRolePermissionInput,
) =>
request.delete<ResponseData<never>>(adminRevokeRolePermissions(role), {
data: params,
});
export const updateUserRole = (username: string, role: string) =>
request.put<ResponseData<never>>(adminUpdateUserRole(username), {
role_name: role,
});
export const getUserPermissions = (username: string) =>
request.get<ResponseData<AdminService.UserDetailWithPermission>>(
adminGetUserPermissions(username),
);
export const listResources = () =>
request.get<ResponseData<AdminService.ResourceType>>(adminListResources);
export default {
login,
logout,
listUsers,
createUser,
showUserDetails: getUserDetails,
updateUserStatus,
updateUserPassword,
deleteUser,
listUserDatasets,
listUserAgents,
};

View File

@ -210,4 +210,47 @@ export default {
removeDataflow: `${api_host}/dataflow/rm`,
listDataflow: `${api_host}/dataflow/list`,
runDataflow: `${api_host}/dataflow/run`,
// admin
adminLogin: `${ExternalApi}${api_host}/admin/login`,
adminLogout: `${ExternalApi}${api_host}/admin/logout`,
adminListUsers: `${ExternalApi}${api_host}/admin/users`,
adminCreateUser: `${ExternalApi}${api_host}/admin/users`,
adminGetUserDetails: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}`,
adminUpdateUserStatus: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}/activate`,
adminUpdateUserPassword: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}/password`,
adminDeleteUser: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}`,
adminListUserDatasets: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}/datasets`,
adminListUserAgents: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}/agents`,
adminListServices: `${ExternalApi}${api_host}/admin/services`,
adminShowServiceDetails: (serviceId: string) =>
`${ExternalApi}${api_host}/admin/services/${serviceId}`,
adminListRoles: `${ExternalApi}${api_host}/admin/roles`,
adminListRolesWithPermission: `${ExternalApi}${api_host}/admin/roles_with_permission`,
adminGetRolePermissions: (roleName: string) =>
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions`,
adminAssignRolePermissions: (roleName: string) =>
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions`,
adminRevokeRolePermissions: (roleName: string) =>
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions/batch`,
adminCreateRole: `${ExternalApi}${api_host}/admin/roles`,
adminDeleteRole: (roleName: string) =>
`${ExternalApi}${api_host}/admin/roles/${roleName}`,
adminUpdateRoleDescription: (roleName: string) =>
`${ExternalApi}${api_host}/admin/roles/${roleName}`,
adminUpdateUserRole: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}/role`,
adminGetUserPermissions: (username: string) =>
`${ExternalApi}${api_host}/admin/users/${username}/permissions`,
adminListResources: `${ExternalApi}${api_host}/admin/roles/resources`,
};

View File

@ -0,0 +1,9 @@
import { Routes } from '@/routes';
import authorizationUtil from '@/utils/authorization-util';
import { Navigate, Outlet } from 'umi';
export default () => {
const isLogin = !!authorizationUtil.getAuthorization();
return isLogin ? <Outlet /> : <Navigate to={Routes.Admin} />;
};