generated from Luis/nextjs-python-web-template
Compare commits
114 Commits
dfd7979baf
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
102e38988c | ||
|
|
4e7c44eeb5 | ||
|
|
3238cdf36d | ||
|
|
1abbc383ec | ||
| 5a09cc8e31 | |||
|
|
c6e1b8a21d | ||
|
|
afe291c54c | ||
| 6024090f69 | |||
|
|
596e87c31b | ||
|
|
06c55f151b | ||
|
|
04675f5e9e | ||
|
|
b5008306cb | ||
|
|
1b549549c0 | ||
|
|
29aa17ca7c | ||
|
|
e06afab048 | ||
|
|
697d2685f3 | ||
|
|
2f6ad476b3 | ||
|
|
dc04001dca | ||
|
|
3828402865 | ||
|
|
5b64eb4c3f | ||
|
|
eebc7eee20 | ||
|
|
2e787aa386 | ||
|
|
90c6df93f4 | ||
| 374fdfdaea | |||
|
|
a240cfd340 | ||
|
|
eaec0feb96 | ||
|
|
f3ab6e1b45 | ||
|
|
2c79dabce0 | ||
|
|
fd09d73edd | ||
|
|
2000c1444a | ||
|
|
b98c6090af | ||
| 42a25a2eda | |||
| b6b2bfbee5 | |||
| 09f80b1f42 | |||
| 170ada9382 | |||
| a8b472e84d | |||
|
|
cf3fc347de | ||
|
|
1d39ebcddc | ||
|
|
d978e413d5 | ||
| 8d72268922 | |||
|
|
840b3b6972 | ||
|
|
db1591a76e | ||
|
|
1d6ea0ef0c | ||
|
|
711ade4866 | ||
|
|
7779441c87 | ||
|
|
8e92415c34 | ||
|
|
68e2f6d683 | ||
|
|
c03da10e98 | ||
| 60205b3c22 | |||
| da99e71b54 | |||
| b978aabdd6 | |||
| 357568ebcb | |||
| ea2be8b7c7 | |||
| 672c8364bc | |||
| 2b8a9d9316 | |||
| 9b6fc699f2 | |||
| d232510c0e | |||
| dcaecba393 | |||
| 7901712c4c | |||
|
|
48df7352da | ||
|
|
c5c4ab7178 | ||
|
|
22bcbf6819 | ||
|
|
2ab2282116 | ||
| 1c6e33e74f | |||
| 1757bf1952 | |||
| c726e4bb41 | |||
| 8a5232255f | |||
| 674168160e | |||
| fba6dcb70a | |||
| 3c6cbe01e8 | |||
| 5aff0ec6f0 | |||
| 03f6318651 | |||
| 11f4651814 | |||
| 1b433ea314 | |||
| 894081a5c8 | |||
| 1f09701795 | |||
| 2fb2a1d22a | |||
| 6f4bab98c1 | |||
|
|
ea0148fdaf | ||
|
|
7ae1d5f768 | ||
|
|
ec67dd1bac | ||
|
|
522d7eb69a | ||
|
|
07a5a2fc24 | ||
| d54206f43c | |||
| 29ca34aaed | |||
| 79dbefcfe6 | |||
|
|
59e33f3ead | ||
| a21d9c1bae | |||
| 17df0a6ac1 | |||
|
|
047bee93c4 | ||
|
|
3052015a51 | ||
| 01e98d363b | |||
|
|
407f569837 | ||
|
|
51859b148b | ||
|
|
13de134bb0 | ||
|
|
9f03c187f3 | ||
|
|
1db032e932 | ||
|
|
7b2b675c2c | ||
|
|
47700b7bd0 | ||
| e3813555cc | |||
| 36004c6151 | |||
| 957f99a906 | |||
| f957e55aeb | |||
| 92e584cc5c | |||
| 93084c360b | |||
| 4918f84e85 | |||
| 422a3f4da1 | |||
| 1980204e82 | |||
| f46ed02435 | |||
| cb31fd79c8 | |||
| eb5eb1613f | |||
| d2af394fc7 | |||
| 26e594f94b | |||
| 2864ec20cc |
@@ -58,7 +58,8 @@ jobs:
|
||||
git commit -am "update ui-assets.nix"
|
||||
|
||||
echo "Current branch: $GITHUB_REF_NAME"
|
||||
git push origin HEAD:$GITHUB_REF_NAME
|
||||
git push origin HEAD:"$GITHUB_REF_NAME"
|
||||
echo "Done uploading"
|
||||
fi
|
||||
else
|
||||
echo "No UI files changed. Skipping asset build and push"
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,3 +23,5 @@ htmlcov
|
||||
# georgs
|
||||
pkgs/.vs/
|
||||
pkgs/clan-cli/.hypothesis/
|
||||
ui-assets.tar.gz
|
||||
ui-release
|
||||
12
.gitlab-ci.yml
Normal file
12
.gitlab-ci.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
stages:
|
||||
- release
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- set -x
|
||||
- ./service-aware-network-front-end/pkgs/clan-cli/push_docker.sh
|
||||
only:
|
||||
- dev
|
||||
tags:
|
||||
- ea
|
||||
146
README.md
146
README.md
@@ -21,7 +21,7 @@ Let's get your development environment up and running:
|
||||
|
||||
1. **Install Nix Package Manager**:
|
||||
|
||||
- You can install the Nix package manager by either [downloading the Nix installer](https://github.com/DeterminateSystems/nix-installer/releases) or running this command:
|
||||
- You can install the Nix package manager by running this command:
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
|
||||
```
|
||||
@@ -34,7 +34,7 @@ sudo echo "experimental-features = nix-command flakes" > '/etc/nix/nix.conf'
|
||||
|
||||
2. **Install direnv**:
|
||||
|
||||
- Download the direnv package from [here](https://direnv.net/docs/installation.html) or run the following command:
|
||||
- Install the direnv package by running the following command:
|
||||
```bash
|
||||
curl -sfL https://direnv.net/install.sh | bash
|
||||
```
|
||||
@@ -75,11 +75,20 @@ sudo echo "experimental-features = nix-command flakes" > '/etc/nix/nix.conf'
|
||||
```bash
|
||||
clan webui --reload --no-open --log-level debug --populate --emulate
|
||||
```
|
||||
- The server will automatically restart if any Python files change.
|
||||
- The server will automatically restart if any Python files change. Emulated services however will not.
|
||||
- The `--populate` flag will automatically populate the database with dummy data
|
||||
- To look into the endpoints open up a swagger instance by visiting: http://localhost:2979/docs
|
||||
- The `--emulate` flag will automatically run servers the database with dummy data for the fronted to communicate with (ap, dlg, c1 and c2)
|
||||
- To look into the emulated endpoints go to http://localhost:2979/emulate
|
||||
|
||||
8. **Build the Frontend**:
|
||||
8. **Detailed Backend Documentation**
|
||||
|
||||
- For detailed backend documentation go to [pkgs/clan-cli/README.md](pkgs/clan-cli/README.md)
|
||||
- We explain:
|
||||
- How to build and run a docker image
|
||||
- Internal workings of the App
|
||||
|
||||
9. **Build the Frontend**:
|
||||
|
||||
- In a different shell, navigate to the `pkgs/ui` directory and execute:
|
||||
```bash
|
||||
@@ -87,31 +96,20 @@ sudo echo "experimental-features = nix-command flakes" > '/etc/nix/nix.conf'
|
||||
```
|
||||
- Wait for the frontend to build.
|
||||
|
||||
9. **Start the Frontend**:
|
||||
- To start the frontend, execute:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- Access the website by going to [http://localhost:3000](http://localhost:3000).
|
||||
10. **Start the Frontend**:
|
||||
|
||||
- To start the frontend, execute:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- Access the website by going to [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
11. **Detailed Frontend Documentation**
|
||||
|
||||
- For detailed frontend documentation go to [pkgs/ui/README.md](pkgs/ui/README.md)
|
||||
|
||||
# Setting Up Your Git Workflow
|
||||
|
||||
Let's set up your Git workflow to collaborate effectively:
|
||||
|
||||
1. **Register Your Gitea Account Locally**:
|
||||
|
||||
- Execute the following command to add your Gitea account locally:
|
||||
```bash
|
||||
tea login add
|
||||
```
|
||||
- Go to https://gitea.gchq.icu/user/settings/applications and create token with all privileges
|
||||
- Fill out the prompt as follows:
|
||||
- URL of Gitea instance: `https://gitea.gchq.icu`
|
||||
- Name of new Login [gitea.gchq.icu]: `gitea.gchq.icu:7171`
|
||||
- Do you have an access token? Yes
|
||||
- Token: \***\*\*\*\***
|
||||
- Set Optional settings: No
|
||||
|
||||
2. **Git Workflow**:
|
||||
|
||||
1. Add your changes to Git using `git add <file1> <file2>`.
|
||||
@@ -124,99 +122,3 @@ Let's set up your Git workflow to collaborate effectively:
|
||||
5. Use `git status` to check for merge conflicts.
|
||||
6. If conflicts exist, resolve them. Here's a tutorial for resolving conflicts in [VSCode](https://code.visualstudio.com/docs/sourcecontrol/overview#_merge-conflicts).
|
||||
7. After resolving conflicts, execute `git merge --continue` and repeat step 5 until there are no conflicts.
|
||||
|
||||
3. **Create a Pull Request**:
|
||||
|
||||
- To automatically open a pull request that gets merged if all tests pass, execute:
|
||||
```bash
|
||||
merge-after-ci
|
||||
```
|
||||
- If it fails and says something along the lines off `[ERROR] fail-on-change` then the formatter complained.
|
||||
Execute `nix fmt` from the project root by hand and then make a new git commit. Afterwards redo step 3 and it should work.
|
||||
|
||||
4. **Review Your Pull Request**:
|
||||
|
||||
- Visit https://gitea.gchq.icu and go to the project page. Check under "Pull Requests" for any issues with your pull request.
|
||||
|
||||
5. **Push Your Changes**:
|
||||
- If there are issues, fix them and redo step 2. Afterward, execute:
|
||||
```bash
|
||||
git push origin HEAD:YourUsername-main
|
||||
```
|
||||
- This will directly push to your open pull request.
|
||||
|
||||
# Debugging
|
||||
|
||||
When working on the backend of your project, debugging is an essential part of the development process. Here are some methods for debugging and testing the backend of your application:
|
||||
|
||||
## Test Backend Locally in Devshell with Breakpoints
|
||||
|
||||
To test the backend locally in a development environment and set breakpoints for debugging, follow these steps:
|
||||
|
||||
1. Run the following command to execute your tests and allow for debugging with breakpoints:
|
||||
```bash
|
||||
pytest -n0 -s --maxfail=1
|
||||
```
|
||||
You can place `breakpoint()` in your Python code where you want to trigger a breakpoint for debugging.
|
||||
|
||||
## Test Backend Locally in a Nix Sandbox
|
||||
|
||||
To run your backend tests in a Nix sandbox, you have two options depending on whether your test functions have been marked as impure or not:
|
||||
|
||||
### Running Tests Marked as Impure
|
||||
|
||||
If your test functions need to execute `nix build` and have been marked as impure because you can't execute `nix build` inside a Nix sandbox, use the following command:
|
||||
|
||||
```bash
|
||||
nix run .#impure-checks
|
||||
```
|
||||
|
||||
This command will run the impure test functions.
|
||||
|
||||
### Running Pure Tests
|
||||
|
||||
For test functions that have not been marked as impure and don't require executing `nix build`, you can use the following command:
|
||||
|
||||
```bash
|
||||
nix build .#checks.x86_64-linux.clan-pytest --rebuild
|
||||
```
|
||||
|
||||
This command will run all pure test functions.
|
||||
|
||||
### Inspecting the Nix Sandbox
|
||||
|
||||
If you need to inspect the Nix sandbox while running tests, follow these steps:
|
||||
|
||||
1. Insert an endless sleep into your test code where you want to pause the execution. For example:
|
||||
|
||||
```python
|
||||
import time
|
||||
time.sleep(3600) # Sleep for one hour
|
||||
```
|
||||
|
||||
2. Use `cntr` and `psgrep` to attach to the Nix sandbox. This allows you to interactively debug your code while it's paused. For example:
|
||||
|
||||
```bash
|
||||
psgrep -a -x your_python_process_name
|
||||
cntr attach <pid>
|
||||
```
|
||||
|
||||
These debugging and testing methods will help you identify and fix issues in your backend code efficiently, ensuring the reliability and robustness of your application.
|
||||
|
||||
# Using this Template
|
||||
|
||||
To make the most of this template:
|
||||
|
||||
1. Set up a new Gitea account named `ui-asset-bot`. Generate an access token with all access permissions and set it under `settings/actions/secrets` as a secret called `BOT_ACCESS_TOKEN`.
|
||||
|
||||
- Also, edit the file `.gitea/workflows/ui_assets.yaml` and change the `BOT_EMAIL` variable to match the email you set for that account. Gitea matches commits to accounts by their email address, so this step is essential.
|
||||
|
||||
2. Create a second Gitea account named `merge-bot`. Edit the file `pkgs/merge-after-ci/default.nix` if the name should be different. Under "Branches," set the main branch to be protected and add `merge-bot` to the whitelisted users for pushing. Set the unprotected file pattern to `**/ui-assets.nix`.
|
||||
|
||||
- Enable the status check for "build / test (pull_request)."
|
||||
|
||||
3. Add both `merge-bot` and `ui-asset-bot` as collaborators.
|
||||
- Set the option to "Delete pull request branch after merge by default."
|
||||
- Also, set the default merge style to "Rebase then create merge commit."
|
||||
|
||||
With this template, you're well-equipped to build and collaborate on high-quality websites efficiently. Happy coding!.
|
||||
|
||||
@@ -1,34 +1,333 @@
|
||||
# clan-cli
|
||||
# Starting The Backend
|
||||
|
||||
The clan-cli contains the command line interface as well as the graphical webui through the `clan webui` command.
|
||||
|
||||
## Hacking on the cli
|
||||
Start the web ui with
|
||||
|
||||
We recommend setting up [direnv](https://direnv.net/) to load the developement with nix.
|
||||
If you do not have it set up you can also use `nix develop` directly like this:
|
||||
|
||||
```
|
||||
use flake .#clan-cli --builders ''
|
||||
```bash
|
||||
clan webui --reload --no-open --log-level debug --populate --emulate
|
||||
```
|
||||
|
||||
After you can use the local bin wrapper to test things in the cli:
|
||||
- The `--populate` flag will automatically populate the database with dummy data
|
||||
- To look into the endpoints open up a swagger instance by visiting: http://localhost:2979/docs
|
||||
- The `--emulate` flag will automatically run servers the database with dummy data for the fronted to communicate with (ap, dlg, c1 and c2)
|
||||
- To look into the emulated endpoints go to http://localhost:2979/emulate
|
||||
|
||||
```
|
||||
./bin/clan
|
||||
# Using the Uploaded Docker Image
|
||||
|
||||
Pull the image
|
||||
|
||||
```bash
|
||||
docker pull git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
|
||||
```
|
||||
|
||||
## Hacking on the webui
|
||||
Run the image
|
||||
|
||||
By default the webui is build from a tarball available https://git.clan.lol/clan/-/packages/generic/ui/.
|
||||
To start a local developement environment instead, use the `--dev` flag:
|
||||
|
||||
```
|
||||
./bin/clan webui --dev
|
||||
```bash
|
||||
docker run -p 127.0.0.1:2979:2979 git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
|
||||
```
|
||||
|
||||
This will spawn two webserver, a python one to for the api and a nodejs one that rebuilds the ui on the fly.
|
||||
# API Documentation
|
||||
|
||||
## Run webui directly
|
||||
Api documentation can be found in the folder `pkgs/clan-cli/tests/openapi_client/docs/`
|
||||
For Entity object go to
|
||||
|
||||
- [tests/openapi_client/docs/EntitiesApi.md](tests/openapi_client/docs/EntitiesApi.md)
|
||||
- [tests/openapi_client/docs/EventmessagesApi.md](tests/openapi_client/docs/EventmessagesApi.md)
|
||||
- [tests/openapi_client/docs/ServicesApi.md](tests/openapi_client/docs/ServicesApi.md)
|
||||
- [tests/openapi_client/docs/ResolutionApi.md](tests/openapi_client/docs/ResolutionApi.md)
|
||||
- [tests/openapi_client/docs/RepositoriesApi.md](tests/openapi_client/docs/RepositoriesApi.md)
|
||||
|
||||
# Building a Docker Image if the Frontend Changed
|
||||
|
||||
To build a new docker image when the frontend code and/or backend code changed you first need
|
||||
to get the `GITLAB_TOKEN` go to [repo access tokens](https://git.tu-berlin.de/internet-of-services-lab/service-aware-network-front-end/-/settings/access_tokens) and generate one.
|
||||
|
||||
- Make sure the Gitlab token has access to package registry.
|
||||
|
||||
Then execute
|
||||
|
||||
```bash
|
||||
export GITLAB_TOKEN="<your-access-token>"
|
||||
```
|
||||
|
||||
Afterwards you can execute:
|
||||
|
||||
```bash
|
||||
./build_docker.sh
|
||||
```
|
||||
|
||||
This will create a symlink directory called `result` to a tar.gz docker file. Import it by executing:
|
||||
|
||||
```bash
|
||||
docker load < result
|
||||
```
|
||||
|
||||
And then run the docker file by executing:
|
||||
|
||||
```bash
|
||||
docker run -p 127.0.0.1:2979:2979 clan-docker:latest
|
||||
```
|
||||
|
||||
# Uploading a Docker Image
|
||||
|
||||
You can use the script:
|
||||
|
||||
```bash
|
||||
./push_docker.sh
|
||||
```
|
||||
|
||||
### The Script Explained
|
||||
|
||||
Login to the tu docker image server
|
||||
|
||||
```bash
|
||||
docker login git.tu-berlin.de:5000
|
||||
```
|
||||
|
||||
Tag the imported image
|
||||
|
||||
```bash
|
||||
docker image tag clan-docker:latest git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
|
||||
```
|
||||
|
||||
Push the image to the git registry
|
||||
|
||||
```bash
|
||||
docker image push git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
|
||||
```
|
||||
|
||||
# Upload UI assets as a package
|
||||
|
||||
To upload the release build UI assets to gitlab as a package
|
||||
first get the `GITLAB_TOKEN`. Go to [repo access tokens](https://git.tu-berlin.de/internet-of-services-lab/service-aware-network-front-end/-/settings/access_tokens) and generate one.
|
||||
|
||||
- Make sure the Gitlab token has access to package registry.
|
||||
|
||||
To upload the UI assets as a package then execute:
|
||||
|
||||
```bash
|
||||
./upload_ui_assets.sh
|
||||
```
|
||||
|
||||
Please commit the changes to ui-assets.nix and push them to the repository.
|
||||
If you want clan webui to use the new ui assets.
|
||||
|
||||
```bash
|
||||
$ git commit -m "Update ui-assets.nix" "$PROJECT_DIR/pkgs/ui/nix/ui-assets.nix"
|
||||
$ git push
|
||||
```
|
||||
|
||||
If you execute `clan webui` the page you will see is a precompiled release version of the UI. This above script will update said precompiled release version. The `./build_docker.sh` script execute this to make sure that the included UI in the docker is up to date.
|
||||
|
||||
### The Script Explained
|
||||
|
||||
If changes to the UI have been made, and you want them to propagate to the docker container and the `clan webui` command edit the file: [../ui/nix/ui-assets.nix](../ui/nix/ui-assets.nix).
|
||||
This is where a release version of the frontend is downloaded and integrated into the cli and the docker build. To do this first execute
|
||||
|
||||
```bash
|
||||
nix build .#ui --out-link ui-release
|
||||
```
|
||||
|
||||
Make a tarball out of it called `ui-assets.tar.gz`
|
||||
|
||||
```bash
|
||||
tar --transform 's,^\.,assets,' -czvf "ui-assets.tar.gz" -C ui-release/result/lib/node_modules/*/out .
|
||||
```
|
||||
|
||||
Upload ui-assets.tar.gz to gitlab.
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
||||
--upload-file ./ui-assets.tar.gz \
|
||||
"https://git.tu-berlin.de/api/v4/projects/internet-of-services-lab%2Fservice-aware-network-front-end/packages/generic/ui-assets/1.0.0/ui-assets.tar.gz"
|
||||
```
|
||||
|
||||
You can find your uploaded package at the [package registry](https://git.tu-berlin.de/internet-of-services-lab/service-aware-network-front-end/-/packages)
|
||||
|
||||
And export the download url into a variable:
|
||||
|
||||
```
|
||||
export url="https://git.tu-berlin.de/api/v4/projects/internet-of-services-lab%2Fservice-aware-network-front-end/packages/generic/ui-assets/1.0.0/ui-assets.tar.gz"
|
||||
```
|
||||
|
||||
Now execute the command:
|
||||
|
||||
```bash
|
||||
cat > "../ui/nix/ui-assets.nix" <<EOF
|
||||
{ fetchzip }:
|
||||
fetchzip {
|
||||
url = "$url";
|
||||
sha256 = "$(nix-prefetch-url --unpack $url)";
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
And now build the docker image:
|
||||
|
||||
```bash
|
||||
nix build .#clan-docker
|
||||
```
|
||||
|
||||
# Building a Docker Image if only the Backend Changed
|
||||
|
||||
To build a new docker image only when the backend code changed execute:
|
||||
|
||||
```bash
|
||||
nix build .#clan-docker
|
||||
```
|
||||
|
||||
This is much faster then the `./build_docker.sh` script as it needs not to build the frontend and again.
|
||||
This will create a symlink directory called `result` to a tar.gz docker file. Import it by executing:
|
||||
|
||||
```bash
|
||||
docker load < result
|
||||
```
|
||||
|
||||
And then run the docker file by executing:
|
||||
|
||||
```bash
|
||||
docker run -p 127.0.0.1:2979:2979 clan-docker:latest
|
||||
```
|
||||
|
||||
- To change parameters in the generated docker image edit the file :
|
||||
[flake-module.nix at line 22](flake-module.nix)
|
||||
- Documentation on `dockerTools.buildImage` you can find here: https://nix.dev/tutorials/nixos/building-and-running-docker-images.html
|
||||
|
||||
# Auto Generating a Python Client
|
||||
|
||||
For the tests we automatically generate a python client for the API endpoints. To do this execute while inside the `pkgs/clan-cli` folder:
|
||||
|
||||
```bash
|
||||
./bin/gen-python-client
|
||||
```
|
||||
|
||||
This will replace the folder
|
||||
`tests/openapi_client`.
|
||||
|
||||
# Adding dependencies
|
||||
|
||||
**Dependency Management**: We use the [Nix package manager](https://nixos.org/) to manage dependencies and ensure reproducibility, making your development process more robust.
|
||||
|
||||
To add dependencies edit the file [default.nix](default.nix)
|
||||
|
||||
To search for a python dependency named "request" execute:
|
||||
|
||||
```bash
|
||||
nix search nixpkgs#pythonPackages request
|
||||
```
|
||||
|
||||
Add the depdendency at the top of the file
|
||||
|
||||
```nix
|
||||
{
|
||||
, mydep # <--- Add here
|
||||
, websockets
|
||||
, broadcaster
|
||||
, aenum
|
||||
, dateutil
|
||||
, urllib3
|
||||
}:
|
||||
let
|
||||
[...]
|
||||
```
|
||||
|
||||
Add them into this array if they are a python dependency
|
||||
|
||||
```nix
|
||||
dependencies = [
|
||||
argcomplete
|
||||
fastapi
|
||||
uvicorn
|
||||
sqlalchemy
|
||||
websockets
|
||||
broadcaster
|
||||
mydep # <--- Add here
|
||||
];
|
||||
```
|
||||
|
||||
To search for a binary dependency named "firefox" execute:
|
||||
|
||||
```bash
|
||||
nix search nixpkgs firefox
|
||||
```
|
||||
|
||||
Runtime dependency add them into this array:
|
||||
|
||||
```nix
|
||||
runtimeDependencies = [
|
||||
bash
|
||||
nix
|
||||
fakeroot
|
||||
zbar
|
||||
git
|
||||
mypy
|
||||
];
|
||||
```
|
||||
|
||||
# Development environment
|
||||
|
||||
The development environment created by `nix develop` or automatically by `direnv` is located at [shell.nix](shell.nix). The `shellHook` variable execute bash code.
|
||||
|
||||
# Debugging
|
||||
|
||||
When working on the backend of your project, debugging is an essential part of the development process. Here are some methods for debugging and testing the backend of your application:
|
||||
|
||||
## Test Backend Locally in Devshell with Breakpoints
|
||||
|
||||
To test the backend locally in a development environment and set breakpoints for debugging, follow these steps:
|
||||
|
||||
1. Run the following command to execute your tests and allow for debugging with breakpoints:
|
||||
```bash
|
||||
rm -f sql_app.db && pytest -s
|
||||
```
|
||||
You can place `breakpoint()` in your Python code where you want to trigger a breakpoint for debugging.
|
||||
|
||||
## Test Backend Locally in a Nix Sandbox
|
||||
|
||||
To run your backend tests in a Nix sandbox, you have two options depending on whether your test functions have been marked as impure or not:
|
||||
|
||||
### Running Tests Marked as Impure
|
||||
|
||||
If your test functions need to execute `nix build` and have been marked as impure because you can't execute `nix build` inside a Nix sandbox, use the following command:
|
||||
|
||||
```bash
|
||||
nix run .#impure-checks
|
||||
```
|
||||
|
||||
This command will run the impure test functions.
|
||||
|
||||
### Running Pure Tests
|
||||
|
||||
For test functions that have not been marked as impure and don't require executing `nix build`, you can use the following command:
|
||||
|
||||
```bash
|
||||
nix build .#checks.x86_64-linux.clan-pytest --rebuild
|
||||
```
|
||||
|
||||
This command will run all pure test functions.
|
||||
|
||||
### Inspecting the Nix Sandbox
|
||||
|
||||
If you need to inspect the Nix sandbox while running tests, follow these steps:
|
||||
|
||||
1. Insert an endless sleep into your test code where you want to pause the execution. For example:
|
||||
|
||||
```python
|
||||
import time
|
||||
time.sleep(3600) # Sleep for one hour
|
||||
```
|
||||
|
||||
2. Use `cntr` and `psgrep` to attach to the Nix sandbox. This allows you to interactively debug your code while it's paused. For example:
|
||||
|
||||
```bash
|
||||
psgrep -a -x your_python_process_name
|
||||
cntr attach <pid>
|
||||
```
|
||||
|
||||
These debugging and testing methods will help you identify and fix issues in your backend code efficiently, ensuring the reliability and robustness of your application.
|
||||
|
||||
## Run Web UI in VSCode
|
||||
|
||||
Useful for vscode run and debug option
|
||||
|
||||
@@ -53,18 +352,3 @@ Add this `launch.json` to your .vscode directory to have working breakpoints in
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Run locally single-threaded for debugging
|
||||
|
||||
By default tests run in parallel using pytest-xdist.
|
||||
pytest-xdist however breaks `breakpoint()`. To disable it, use this:
|
||||
|
||||
```console
|
||||
pytest -n0 -s
|
||||
```
|
||||
|
||||
You can also run a single test like this:
|
||||
|
||||
```console
|
||||
pytest -n0 -s tests/test_secrets_cli.py::test_users
|
||||
```
|
||||
|
||||
16
pkgs/clan-cli/build_docker.sh
Executable file
16
pkgs/clan-cli/build_docker.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR=$(git rev-parse --show-toplevel)
|
||||
"$PROJECT_DIR"/pkgs/clan-cli/upload_ui_assets.sh
|
||||
|
||||
nix build .#clan-docker
|
||||
|
||||
cat <<EOF
|
||||
==============================
|
||||
Please commit the changes to ui-assets.nix and push them to the repository.
|
||||
If you want clan webui to use the new ui assets.
|
||||
$ git commit -m "Update ui-assets.nix" "$PROJECT_DIR/pkgs/ui/nix/ui-assets.nix"
|
||||
$ git push
|
||||
EOF
|
||||
37
pkgs/clan-cli/clan_cli/README.md
Normal file
37
pkgs/clan-cli/clan_cli/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
**init\_**.py:
|
||||
|
||||
```bash
|
||||
usage: clan webui [-h] [--port PORT] [--host HOST] [--populate] [--emulate] [--no-open] [--dev]
|
||||
[--dev-port DEV_PORT] [--dev-host DEV_HOST] [--reload]
|
||||
[--log-level {critical,error,warning,info,debug,trace}]
|
||||
[sub_url]
|
||||
|
||||
positional arguments:
|
||||
sub_url Sub URL to open in the browser
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--port PORT Port to listen on
|
||||
--host HOST Host to listen on
|
||||
--populate Populate the database with dummy data
|
||||
--emulate Emulate two entities c1 and c2 + dlg and ap
|
||||
--no-open Don't open the browser
|
||||
--dev Run in development mode
|
||||
--dev-port DEV_PORT Port to listen on for the dev server
|
||||
--dev-host DEV_HOST Host to listen on
|
||||
--reload Don't reload on changes
|
||||
--log-level {critical,error,warning,info,debug,trace}
|
||||
Log level
|
||||
```
|
||||
|
||||
In this folder are some basic files:
|
||||
|
||||
- config.py
|
||||
- to configer basic value for the server and the emulation
|
||||
- ip/host
|
||||
- ports
|
||||
- emuplate_fast.py
|
||||
- some api call that emulate the behavoir
|
||||
- extra servers with api calls are emulated here
|
||||
|
||||
In the subfolder <webui> is the backend impplemented.
|
||||
@@ -1,14 +1,18 @@
|
||||
# Imports
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Optional
|
||||
|
||||
# Custom imports
|
||||
from . import webui
|
||||
from .custom_logger import setup_logging
|
||||
|
||||
# Setting up the logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Trying to import argcomplete module, if not present, set it to None
|
||||
argcomplete: Optional[ModuleType] = None
|
||||
try:
|
||||
import argcomplete # type: ignore[no-redef]
|
||||
@@ -16,44 +20,59 @@ except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Function to create the main argument parser
|
||||
def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser:
|
||||
# Creating the main argument parser with a description
|
||||
parser = argparse.ArgumentParser(prog=prog, description="cLAN tool")
|
||||
|
||||
# Adding a debug argument to enable debug logging
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
help="Enable debug logging",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
# Adding subparsers for different commands
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
# Adding a subparser for the "webui" command
|
||||
parser_webui = subparsers.add_parser("webui", help="start webui")
|
||||
# Registering additional arguments for the "webui" command
|
||||
webui.register_parser(parser_webui)
|
||||
|
||||
# Using argcomplete for shell autocompletion if available
|
||||
if argcomplete:
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
# If no command-line arguments provided, print the help message
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
return parser
|
||||
|
||||
|
||||
# this will be the entrypoint under /bin/clan (see pyproject.toml config)
|
||||
### Main entry point function
|
||||
def main() -> None:
|
||||
# Creating the main argument parser
|
||||
parser = create_parser()
|
||||
# Parsing command-line arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setting up logging based on the debug flag
|
||||
if args.debug:
|
||||
setup_logging(logging.DEBUG)
|
||||
log.debug("Debug log activated")
|
||||
else:
|
||||
setup_logging(logging.INFO)
|
||||
|
||||
# If the parsed arguments do not have the "func" attribute, exit
|
||||
if not hasattr(args, "func"):
|
||||
return
|
||||
|
||||
# Calling the function associated with the specified command
|
||||
args.func(args)
|
||||
|
||||
|
||||
# Entry point for script execution
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,8 +1,88 @@
|
||||
# CORS configuration
|
||||
cors_url = [
|
||||
"http://localhost",
|
||||
"http://127.0.0.1",
|
||||
"http://0.0.0.0",
|
||||
"http://[::]",
|
||||
]
|
||||
cors_ports = ["*", 3000, 2979, 8001, 8002]
|
||||
cors_whitelist = []
|
||||
for u in cors_url:
|
||||
for p in cors_ports:
|
||||
cors_whitelist.append(f"{u}:{p}")
|
||||
|
||||
# host for the server, frontend, backend and emulators
|
||||
host = "127.0.0.1"
|
||||
|
||||
# Used for eventmessage number to name mapping
|
||||
group_type_to_label = {
|
||||
1: {
|
||||
"name": "Attachement",
|
||||
1: "Request Send",
|
||||
2: "Request Received",
|
||||
3: "Response Send",
|
||||
4: "Response Received",
|
||||
},
|
||||
2: {
|
||||
"name": "Connection Setup",
|
||||
1: "Request Send",
|
||||
2: "Request Received",
|
||||
3: "Response Send",
|
||||
4: "Response Received",
|
||||
},
|
||||
3: {
|
||||
"name": "Presentation",
|
||||
1: "Request Send",
|
||||
2: "Request Received",
|
||||
3: "Respone Send",
|
||||
4: "Respone Received",
|
||||
5: "Respone Ack",
|
||||
},
|
||||
4: {
|
||||
"name": "DID Resolution",
|
||||
1: "Request Send",
|
||||
2: "Request Received",
|
||||
3: "Response Send",
|
||||
4: "Response Received",
|
||||
},
|
||||
5: {
|
||||
"name": "Service De-registration",
|
||||
1: "Send",
|
||||
2: "Received",
|
||||
3: "Success Send",
|
||||
4: "Success Received",
|
||||
},
|
||||
6: {
|
||||
"name": "Service Registration",
|
||||
1: "Send",
|
||||
2: "Received",
|
||||
3: "Success Send",
|
||||
4: "Success Received",
|
||||
},
|
||||
7: {
|
||||
"name": "Service Discovery",
|
||||
1: "Discovery Send",
|
||||
2: "Discovery Received",
|
||||
3: "Result Send",
|
||||
4: "Result Received",
|
||||
},
|
||||
8: {
|
||||
"name": "Service Operation",
|
||||
1: "Request Send",
|
||||
2: "Request Received",
|
||||
3: "Response Send",
|
||||
4: "Response Received",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Used for emulation and population for testing
|
||||
port_dlg = 7000
|
||||
port_ap = 7500
|
||||
port_client_base = 8000
|
||||
_port_client_base = 8000
|
||||
c1_port = _port_client_base + 1
|
||||
c2_port = _port_client_base + 2
|
||||
dlg_url = f"http://{host}:{port_dlg}/docs"
|
||||
ap_url = f"http://{host}:{port_ap}/docs"
|
||||
c1_url = f"http://{host}:{port_client_base}/docs"
|
||||
c2_url = f"http://{host}:{port_client_base + 1}/docs"
|
||||
c1_url = f"http://{host}:{c1_port}/docs"
|
||||
c2_url = f"http://{host}:{c2_port}/docs"
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
# Importing necessary modules and packages
|
||||
import sys
|
||||
import time
|
||||
import urllib
|
||||
from datetime import datetime
|
||||
|
||||
# Importing FastAPI and related components
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
# Importing configuration and schemas from the clan_cli package
|
||||
import clan_cli.config as config
|
||||
from clan_cli.webui.schemas import Resolution
|
||||
|
||||
# Creating FastAPI instances for different applications
|
||||
app_dlg = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
|
||||
app_ap = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
|
||||
app_c1 = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
|
||||
app_c2 = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
|
||||
|
||||
# List of FastAPI instances and their associated ports
|
||||
apps = [
|
||||
(app_dlg, config.port_dlg),
|
||||
(app_ap, config.port_ap),
|
||||
(app_c1, config.port_client_base),
|
||||
(app_c2, config.port_client_base + 1),
|
||||
(app_c1, config.c1_port),
|
||||
(app_c2, config.c2_port),
|
||||
]
|
||||
for app, port in apps:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config.cors_whitelist,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
#### HEALTHCHECK
|
||||
# Healthcheck endpoints for different applications
|
||||
@app_c1.get("/")
|
||||
async def root_c1() -> str:
|
||||
return "C1 is alive"
|
||||
@@ -63,6 +77,7 @@ async def healthcheck_ap() -> str:
|
||||
return "200 OK"
|
||||
|
||||
|
||||
# Function for performing health checks on a given URL with retries
|
||||
def get_health(*, url: str, max_retries: int = 20, delay: float = 0.2) -> str | None:
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
@@ -74,13 +89,10 @@ def get_health(*, url: str, max_retries: int = 20, delay: float = 0.2) -> str |
|
||||
return None
|
||||
|
||||
|
||||
#### CONSUME SERVICE
|
||||
|
||||
# TODO send_msg???
|
||||
|
||||
|
||||
@app_c1.get("/consume_service_from_other_entity", response_class=HTMLResponse)
|
||||
# Service consumption emulation for c1 which returns a gif1
|
||||
@app_c1.get("/v1/print_daemon1", response_class=HTMLResponse)
|
||||
async def consume_service_from_other_entity_c1() -> HTMLResponse:
|
||||
# HTML content for the response
|
||||
html_content = """
|
||||
<html>
|
||||
<body>
|
||||
@@ -92,8 +104,21 @@ async def consume_service_from_other_entity_c1() -> HTMLResponse:
|
||||
return HTMLResponse(content=html_content, status_code=200)
|
||||
|
||||
|
||||
@app_c2.get("/consume_service_from_other_entity", response_class=HTMLResponse)
|
||||
@app_c1.get("/v1/print_daemon1/register", response_class=JSONResponse)
|
||||
async def register_c1() -> JSONResponse:
|
||||
time.sleep(2)
|
||||
return JSONResponse(content={"status": "registered"}, status_code=200)
|
||||
|
||||
|
||||
@app_c1.get("/v1/print_daemon1/deregister", response_class=JSONResponse)
|
||||
async def deregister_c1() -> JSONResponse:
|
||||
time.sleep(2)
|
||||
return JSONResponse(content={"status": "deregistered"}, status_code=200)
|
||||
|
||||
|
||||
@app_c2.get("/v1/print_daemon2", response_class=HTMLResponse)
|
||||
async def consume_service_from_other_entity_c2() -> HTMLResponse:
|
||||
# Similar HTML content for the response
|
||||
html_content = """
|
||||
<html>
|
||||
<body>
|
||||
@@ -105,51 +130,113 @@ async def consume_service_from_other_entity_c2() -> HTMLResponse:
|
||||
return HTMLResponse(content=html_content, status_code=200)
|
||||
|
||||
|
||||
@app_c2.get("/v1/print_daemon2/register", response_class=JSONResponse)
|
||||
async def register_c2() -> JSONResponse:
|
||||
time.sleep(2)
|
||||
return JSONResponse(content={"status": "registered"}, status_code=200)
|
||||
|
||||
|
||||
@app_c2.get("/v1/print_daemon2/deregister", response_class=JSONResponse)
|
||||
async def deregister_c2() -> JSONResponse:
|
||||
time.sleep(2)
|
||||
return JSONResponse(content={"status": "deregistered"}, status_code=200)
|
||||
|
||||
|
||||
@app_ap.get("/ap_list_of_services", response_class=JSONResponse)
|
||||
async def ap_list_of_services() -> JSONResponse:
|
||||
# Sample list of services as a JSON response
|
||||
res = [
|
||||
# Service 1
|
||||
{
|
||||
"uuid": "98ae4334-6c12-ace8-ae34-0454cac5b68c",
|
||||
"service_name": "Carlos Printing46",
|
||||
"uuid": "bdd640fb-0667-1ad1-1c80-317fa3b1799d",
|
||||
"service_name": "Carlos Printing0",
|
||||
"service_type": "3D Printing",
|
||||
"endpoint_url": "127.0.0.1:6600/v1/print_daemon46",
|
||||
"status": "unknown",
|
||||
"other": {"action": ["register", "deregister", "delete", "create"]},
|
||||
"entity_did": "did:sov:test:6600",
|
||||
"entity": {
|
||||
"did": "did:sov:test:6600",
|
||||
"name": "AP",
|
||||
"ip": "127.0.0.1:6600",
|
||||
"network": "255.255.0.0",
|
||||
"visible": True,
|
||||
"other": {},
|
||||
"attached": False,
|
||||
"stop_health_task": False,
|
||||
"roles": ["AP"],
|
||||
"endpoint_url": "http://127.0.0.1:8001/v1/print_daemon1",
|
||||
"other": {},
|
||||
"entity_did": "did:sov:test:120",
|
||||
"status": {"data": ["draft", "registered"]},
|
||||
"action": {
|
||||
"data": [
|
||||
{
|
||||
"name": "register",
|
||||
"endpoint": "http://127.0.0.1:8001/v1/print_daemon1/register",
|
||||
},
|
||||
{
|
||||
"name": "deregister",
|
||||
"endpoint": "http://127.0.0.1:8001/v1/print_daemon1/deregister",
|
||||
},
|
||||
]
|
||||
},
|
||||
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
|
||||
},
|
||||
# Service 2 (similar structure)
|
||||
{
|
||||
"uuid": "23b8c1e9-3924-56de-3eb1-3b9046685257",
|
||||
"service_name": "Carlos Printing1",
|
||||
"service_type": "3D Printing",
|
||||
"endpoint_url": "http://127.0.0.1:8002/v1/print_daemon2",
|
||||
"other": {},
|
||||
"entity_did": "did:sov:test:121",
|
||||
"status": {"data": ["draft", "registered"]},
|
||||
"action": {
|
||||
"data": [
|
||||
{
|
||||
"name": "register",
|
||||
"endpoint": "http://127.0.0.1:8002/v1/print_daemon2/register",
|
||||
},
|
||||
{
|
||||
"name": "deregister",
|
||||
"endpoint": "http://127.0.0.1:8002/v1/print_daemon2/deregister",
|
||||
},
|
||||
]
|
||||
},
|
||||
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
|
||||
},
|
||||
{
|
||||
"uuid": "988c24c9-61b1-cd22-6280-1c4510435a10",
|
||||
"service_name": "Carlos Printing47",
|
||||
"uuid": "bd9c66b3-ad3c-2d6d-1a3d-1fa7bc8960a9",
|
||||
"service_name": "Carlos Printing2",
|
||||
"service_type": "3D Printing",
|
||||
"endpoint_url": "127.0.0.1:6600/v1/print_daemon47",
|
||||
"status": "unknown",
|
||||
"other": {"action": ["register", "deregister", "delete", "create"]},
|
||||
"entity_did": "did:sov:test:6600",
|
||||
"entity": {
|
||||
"did": "did:sov:test:6600",
|
||||
"name": "AP",
|
||||
"ip": "127.0.0.1:6600",
|
||||
"network": "255.255.0.0",
|
||||
"visible": True,
|
||||
"other": {},
|
||||
"attached": False,
|
||||
"stop_health_task": False,
|
||||
"roles": ["AP"],
|
||||
"endpoint_url": "http://127.0.0.1:8003/v1/print_daemon3",
|
||||
"other": {},
|
||||
"entity_did": "did:sov:test:122",
|
||||
"status": {"data": ["draft", "registered"]},
|
||||
"action": {
|
||||
"data": [
|
||||
{
|
||||
"name": "register",
|
||||
"endpoint": "http://127.0.0.1:8003/v1/print_daemon3/register",
|
||||
},
|
||||
{
|
||||
"name": "deregister",
|
||||
"endpoint": "http://127.0.0.1:8003/v1/print_daemon3/deregister",
|
||||
},
|
||||
]
|
||||
},
|
||||
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
|
||||
},
|
||||
{
|
||||
"uuid": "972a8469-1641-9f82-8b9d-2434e465e150",
|
||||
"service_name": "Carlos Printing3",
|
||||
"service_type": "3D Printing",
|
||||
"endpoint_url": "http://127.0.0.1:8004/v1/print_daemon4",
|
||||
"other": {},
|
||||
"entity_did": "did:sov:test:123",
|
||||
"status": {"data": ["draft", "registered"]},
|
||||
"action": {
|
||||
"data": [
|
||||
{
|
||||
"name": "register",
|
||||
"endpoint": "http://127.0.0.1:8004/v1/print_daemon4/register",
|
||||
},
|
||||
{
|
||||
"name": "deregister",
|
||||
"endpoint": "http://127.0.0.1:8004/v1/print_daemon4/deregister",
|
||||
},
|
||||
]
|
||||
},
|
||||
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
|
||||
},
|
||||
]
|
||||
# resp = json.dumps(obj=res)
|
||||
return JSONResponse(content=res, status_code=200)
|
||||
|
||||
|
||||
|
||||
6
pkgs/clan-cli/clan_cli/webui/README.md
Normal file
6
pkgs/clan-cli/clan_cli/webui/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Here are the files found for the backend of the service.
|
||||
|
||||
- the backed is using a sql light db with sqlachremy
|
||||
- this is done in
|
||||
- sql\_\*.py, schema.py, tags.py
|
||||
- subfolder: routers which also contains the apicall defenitions
|
||||
@@ -2,25 +2,55 @@ import argparse
|
||||
import logging
|
||||
from typing import Callable, NoReturn, Optional
|
||||
|
||||
# Get the logger for this module
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Initialize variables for server startup and potential ImportError
|
||||
start_server: Optional[Callable] = None
|
||||
ServerImportError: Optional[ImportError] = None
|
||||
|
||||
# Try importing the start_server function from the server module
|
||||
try:
|
||||
from .server import start_server
|
||||
except ImportError as e:
|
||||
# If ImportError occurs, log the exception and store it in ServerImportError
|
||||
log.exception(e)
|
||||
ServerImportError = e
|
||||
|
||||
|
||||
# Function to be called when FastAPI is not installed
|
||||
##########################################################################################
|
||||
# usage: clan webui [-h] [--port PORT] [--host HOST] [--populate] [--emulate] [--no-open] [--dev]
|
||||
# [--dev-port DEV_PORT] [--dev-host DEV_HOST] [--reload]
|
||||
# [--log-level {critical,error,warning,info,debug,trace}]
|
||||
# [sub_url]
|
||||
#
|
||||
# positional arguments:
|
||||
# sub_url Sub URL to open in the browser
|
||||
#
|
||||
# options:
|
||||
# -h, --help show this help message and exit
|
||||
# --port PORT Port to listen on
|
||||
# --host HOST Host to listen on
|
||||
# --populate Populate the database with dummy data
|
||||
# --emulate Emulate two entities c1 and c2 + dlg and ap
|
||||
# --no-open Don't open the browser
|
||||
# --dev Run in development mode
|
||||
# --dev-port DEV_PORT Port to listen on for the dev server
|
||||
# --dev-host DEV_HOST Host to listen on
|
||||
# --reload Don't reload on changes
|
||||
# --log-level {critical,error,warning,info,debug,trace}
|
||||
# Log level
|
||||
##########################################################################################
|
||||
def fastapi_is_not_installed(_: argparse.Namespace) -> NoReturn:
|
||||
assert ServerImportError is not None
|
||||
print(
|
||||
f"Dependencies for the webserver is not installed. The webui command has been disabled ({ServerImportError})"
|
||||
f"Dependencies for the webserver are not installed. The webui command has been disabled ({ServerImportError})"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
|
||||
# Function to register command-line arguments for the webserver
|
||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--port", type=int, default=2979, help="Port to listen on")
|
||||
parser.add_argument(
|
||||
@@ -69,10 +99,10 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
type=str,
|
||||
default="/",
|
||||
nargs="?",
|
||||
help="Sub url to open in the browser",
|
||||
help="Sub URL to open in the browser",
|
||||
)
|
||||
|
||||
# Set the args.func variable in args
|
||||
# Set the args.func variable in args based on whether FastAPI is installed
|
||||
if start_server is None:
|
||||
parser.set_defaults(func=fastapi_is_not_installed)
|
||||
else:
|
||||
|
||||
@@ -1,55 +1,46 @@
|
||||
# Imports
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
# import for sql
|
||||
# Import FastAPI components and SQLAlchemy related modules
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
# Import configs
|
||||
from ..config import cors_ports, cors_url
|
||||
|
||||
# Import custom modules and classes
|
||||
from ..errors import ClanError
|
||||
from . import sql_models
|
||||
from .assets import asset_path
|
||||
from .error_handlers import clan_error_handler, sql_error_handler
|
||||
from .routers import endpoints, health, root, socket_manager2 # sql router hinzufügen
|
||||
from .routers import endpoints, health, root
|
||||
from .sql_db import engine
|
||||
from .tags import tags_metadata
|
||||
|
||||
cors_url = [
|
||||
"http://localhost",
|
||||
"http://127.0.0.1",
|
||||
"http://0.0.0.0",
|
||||
"http://[::]",
|
||||
]
|
||||
cors_ports = [2979, 3000]
|
||||
cors_whitelist = []
|
||||
for u in cors_url:
|
||||
for p in cors_ports:
|
||||
cors_whitelist.append(f"{u}:{p}")
|
||||
|
||||
|
||||
# Logging setup
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> Any:
|
||||
await socket_manager2.brd.connect()
|
||||
yield
|
||||
await socket_manager2.brd.disconnect()
|
||||
|
||||
|
||||
# Function to set up and configure the FastAPI application
|
||||
def setup_app() -> FastAPI:
|
||||
# bind sql engine
|
||||
# TODO comment aut and add flag to run with pupulated data rm *.sql run pytest with marked then start clan webui
|
||||
# https://docs.pytest.org/en/7.1.x/example/markers.html
|
||||
# Uncomment the following line to drop existing tables during startup (if needed)
|
||||
# sql_models.Base.metadata.drop_all(engine)
|
||||
|
||||
# Create tables in the database using SQLAlchemy
|
||||
sql_models.Base.metadata.create_all(bind=engine)
|
||||
|
||||
app = FastAPI(lifespan=lifespan, swagger_ui_parameters={"tryItOutEnabled": True})
|
||||
# Initialize FastAPI application with lifespan management
|
||||
app = FastAPI(swagger_ui_parameters={"tryItOutEnabled": True})
|
||||
|
||||
# Configure CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=cors_whitelist,
|
||||
@@ -58,31 +49,35 @@ def setup_app() -> FastAPI:
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers for various endpoints and components
|
||||
app.include_router(health.router)
|
||||
# sql methodes
|
||||
app.include_router(endpoints.router)
|
||||
|
||||
app.include_router(socket_manager2.router)
|
||||
|
||||
# Needs to be last in register. Because of wildcard route
|
||||
# Needs to be last in registration due to wildcard route
|
||||
app.include_router(root.router)
|
||||
|
||||
# Add custom exception handlers
|
||||
app.add_exception_handler(ClanError, clan_error_handler) # type: ignore
|
||||
app.add_exception_handler(SQLAlchemyError, sql_error_handler) # type: ignore
|
||||
|
||||
# Mount the "static" route for serving static files
|
||||
app.mount("/static", StaticFiles(directory=asset_path()), name="static")
|
||||
|
||||
# Add tag descriptions to the OpenAPI schema
|
||||
app.openapi_tags = tags_metadata
|
||||
|
||||
# Assign operation IDs to API routes
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
route.operation_id = route.name # in this case, 'read_items'
|
||||
log.debug(f"Registered route: {route}")
|
||||
|
||||
# Log registered exception handlers
|
||||
for i in app.exception_handlers.items():
|
||||
log.debug(f"Registered exception handler: {i}")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
# Create an instance of the FastAPI application
|
||||
app = setup_app()
|
||||
|
||||
1
pkgs/clan-cli/clan_cli/webui/routers/README.md
Normal file
1
pkgs/clan-cli/clan_cli/webui/routers/README.md
Normal file
@@ -0,0 +1 @@
|
||||
In the <endpoints.py> are the api endpoints implemented which could be used of the user/s.
|
||||
@@ -1,12 +1,17 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, List
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Query
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from clan_cli.config import ap_url, c1_url, c2_url, dlg_url, group_type_to_label
|
||||
|
||||
from ...errors import ClanError
|
||||
from .. import sql_crud, sql_db, sql_models
|
||||
from ..schemas import (
|
||||
@@ -27,11 +32,21 @@ router = APIRouter()
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# API Endpoints for all tables
|
||||
# see the default api documentation under:
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/DefaultApi.md
|
||||
|
||||
|
||||
#########################
|
||||
# #
|
||||
# Service #
|
||||
# #
|
||||
#########################
|
||||
# see the corresponding documentation under:
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/Service.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/ServiceCreate.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/ServiceUsageCreate.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/ServicesApi.md
|
||||
@router.post("/api/v1/service", response_model=Service, tags=[Tags.services])
|
||||
def create_service(
|
||||
service: ServiceCreate, db: Session = Depends(sql_db.get_db)
|
||||
@@ -98,10 +113,8 @@ def get_service_by_uuid(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(sql_db.get_db),
|
||||
) -> sql_models.Service:
|
||||
) -> Optional[sql_models.Service]:
|
||||
service = sql_crud.get_service_by_uuid(db, uuid=uuid)
|
||||
if service is None:
|
||||
raise ClanError(f"Service with uuid '{uuid}' not found")
|
||||
return service
|
||||
|
||||
|
||||
@@ -134,6 +147,10 @@ def delete_service(
|
||||
# Entity #
|
||||
# #
|
||||
#########################
|
||||
# see the corresponding documentation under:
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/Entity.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/EntityCreate.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/EntitiesApi.md
|
||||
@router.post("/api/v1/entity", response_model=Entity, tags=[Tags.entities])
|
||||
def create_entity(
|
||||
entity: EntityCreate, db: Session = Depends(sql_db.get_db)
|
||||
@@ -151,6 +168,14 @@ def get_entity_by_roles(
|
||||
return entity
|
||||
|
||||
|
||||
@router.get("/api/v1/entity_by_role", response_model=List[Entity], tags=[Tags.entities])
|
||||
def get_entity_by_role(
|
||||
role: Role, db: Session = Depends(sql_db.get_db)
|
||||
) -> List[sql_models.Entity]:
|
||||
entity = sql_crud.get_entity_by_role(db, roles=[role])
|
||||
return entity
|
||||
|
||||
|
||||
@router.get("/api/v1/entities", response_model=List[Entity], tags=[Tags.entities])
|
||||
def get_all_entities(
|
||||
skip: int = 0, limit: int = 100, db: Session = Depends(sql_db.get_db)
|
||||
@@ -163,10 +188,8 @@ def get_all_entities(
|
||||
def get_entity_by_did(
|
||||
entity_did: str = "did:sov:test:120",
|
||||
db: Session = Depends(sql_db.get_db),
|
||||
) -> sql_models.Entity:
|
||||
) -> Optional[sql_models.Entity]:
|
||||
entity = sql_crud.get_entity_by_name_or_did(db, name=entity_did)
|
||||
if entity is None:
|
||||
raise ClanError(f"Entity with did '{entity_did}' not found")
|
||||
return entity
|
||||
|
||||
|
||||
@@ -298,6 +321,9 @@ def get_rpc_by_role(db: Session, role: Role, path: str) -> Any:
|
||||
# Resolution #
|
||||
# #
|
||||
#########################
|
||||
# see the corresponding documentation under:
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/Resolution.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/ResolutionApi.md
|
||||
@router.get(
|
||||
"/api/v1/resolutions", response_model=List[Resolution], tags=[Tags.resolutions]
|
||||
)
|
||||
@@ -312,6 +338,8 @@ def get_all_resolutions(
|
||||
# Repository #
|
||||
# #
|
||||
#########################
|
||||
# see the corresponding documentation under:
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/RepositoriesApi.md
|
||||
@router.get(
|
||||
"/api/v1/repositories", tags=[Tags.repositories], response_model=List[Service]
|
||||
)
|
||||
@@ -326,6 +354,10 @@ def get_all_repositories(
|
||||
# Eventmessage #
|
||||
# #
|
||||
#########################
|
||||
# see the corresponding documentation under:
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/Eventmessage.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/EventmessageCreate.md
|
||||
### pkgs/clan-cli/tests/openapi_client/docs/EventmessageApi.md
|
||||
@router.post(
|
||||
"/api/v1/event_message", response_model=Eventmessage, tags=[Tags.eventmessages]
|
||||
)
|
||||
@@ -335,16 +367,75 @@ def create_eventmessage(
|
||||
return sql_crud.create_eventmessage(db, eventmsg)
|
||||
|
||||
|
||||
@typing.no_type_check
|
||||
@router.get(
|
||||
"/api/v1/event_messages",
|
||||
response_model=List[Eventmessage],
|
||||
response_class=PlainTextResponse,
|
||||
tags=[Tags.eventmessages],
|
||||
)
|
||||
def get_all_eventmessages(
|
||||
skip: int = 0, limit: int = 100, db: Session = Depends(sql_db.get_db)
|
||||
) -> List[sql_models.Eventmessage]:
|
||||
) -> PlainTextResponse:
|
||||
# SQL sorts eventmessages by timestamp, so we don't need to sort them here
|
||||
eventmessages = sql_crud.get_eventmessages(db, skip=skip, limit=limit)
|
||||
return eventmessages
|
||||
cresult: List[OrderedDict[int, OrderedDict[int, List[Eventmessage]]]] = []
|
||||
|
||||
cresult_idx = 0
|
||||
cresult.append(OrderedDict())
|
||||
for idx, msg in enumerate(eventmessages):
|
||||
# Use the group_type_to_label from config.py to get the group name and msg_type name
|
||||
group = group_type_to_label.get(msg.group, None)
|
||||
group_name = (
|
||||
str(group.get("name", None)) if group is not None else str(msg.group)
|
||||
)
|
||||
msg_type_name = (
|
||||
group.get(msg.msg_type, None) if group is not None else str(msg.msg_type)
|
||||
)
|
||||
|
||||
# Get the name of the src and des entity from the database
|
||||
src_name = sql_crud.get_entity_by_did(db, msg.src_did)
|
||||
src_name = msg.src_did if src_name is None else src_name.name
|
||||
des_name = sql_crud.get_entity_by_did(db, msg.des_did)
|
||||
des_name = msg.des_did if des_name is None else des_name.name
|
||||
|
||||
result = cresult[cresult_idx]
|
||||
|
||||
if result.get("group_name") is None:
|
||||
# Initialize the result array and dictionary
|
||||
result["group_name"] = group_name
|
||||
elif result["group_name"] != group_name:
|
||||
# If the group name changed, create a new result array and dictionary
|
||||
cresult_idx += 1
|
||||
cresult.append(OrderedDict())
|
||||
result = cresult[cresult_idx]
|
||||
result["group_name"] = group_name
|
||||
|
||||
if result.get("groups") is None:
|
||||
result["groups"] = OrderedDict()
|
||||
|
||||
if result["groups"].get(msg.group_id) is None:
|
||||
result["groups"][msg.group_id] = []
|
||||
|
||||
# Append the eventmessage to the result array
|
||||
result_arr = result["groups"][msg.group_id]
|
||||
result_arr.append(
|
||||
Eventmessage(
|
||||
id=msg.id,
|
||||
timestamp=msg.timestamp,
|
||||
group=msg.group,
|
||||
group_name=group_name,
|
||||
group_id=msg.group_id,
|
||||
msg_type=msg.msg_type,
|
||||
msg_type_name=msg_type_name,
|
||||
src_did=msg.src_did,
|
||||
src_name=src_name,
|
||||
des_did=msg.des_did,
|
||||
des_name=des_name,
|
||||
msg=msg.msg,
|
||||
).dict()
|
||||
)
|
||||
|
||||
return PlainTextResponse(content=json.dumps(cresult, indent=4), status_code=200)
|
||||
|
||||
|
||||
##############################
|
||||
@@ -354,8 +445,6 @@ def get_all_eventmessages(
|
||||
##############################
|
||||
@router.get("/emulate", response_class=HTMLResponse)
|
||||
def get_emulated_enpoints() -> HTMLResponse:
|
||||
from clan_cli.config import ap_url, c1_url, c2_url, dlg_url
|
||||
|
||||
html_content = f"""
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Requires: `starlette`, `uvicorn`, `jinja2`
|
||||
# Run with `uvicorn example:app`
|
||||
import logging
|
||||
import os
|
||||
|
||||
import anyio
|
||||
from broadcaster import Broadcast
|
||||
from fastapi import APIRouter, WebSocket
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
brd = Broadcast("memory://")
|
||||
|
||||
|
||||
@router.get("/ws2_example")
|
||||
async def get() -> HTMLResponse:
|
||||
html = open(f"{os.getcwd()}/webui/routers/messenger.html").read()
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@router.websocket("/ws2")
|
||||
async def chatroom_ws(websocket: WebSocket) -> None:
|
||||
await websocket.accept()
|
||||
|
||||
async with anyio.create_task_group() as task_group:
|
||||
# run until first is complete
|
||||
async def run_chatroom_ws_receiver() -> None:
|
||||
await chatroom_ws_receiver(websocket=websocket)
|
||||
task_group.cancel_scope.cancel()
|
||||
|
||||
task_group.start_soon(run_chatroom_ws_receiver)
|
||||
log.warning("Started chatroom_ws_sender")
|
||||
|
||||
await chatroom_ws_sender(websocket)
|
||||
|
||||
|
||||
async def chatroom_ws_receiver(websocket: WebSocket) -> None:
|
||||
async for message in websocket.iter_text():
|
||||
log.warning(f"Received message: {message}")
|
||||
await brd.publish(channel="chatroom", message=message)
|
||||
|
||||
|
||||
async def chatroom_ws_sender(websocket: WebSocket) -> None:
|
||||
async with brd.subscribe(channel="chatroom") as subscriber:
|
||||
if subscriber is None:
|
||||
log.error("Subscriber is None")
|
||||
return
|
||||
async for event in subscriber: # type: ignore
|
||||
await websocket.send_text(event.message)
|
||||
@@ -1,20 +1,30 @@
|
||||
# Imports
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
from . import sql_models
|
||||
from .db_types import Role, Status
|
||||
|
||||
# Set logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# create basemodel
|
||||
class Machine(BaseModel):
|
||||
name: str
|
||||
status: Status
|
||||
|
||||
|
||||
### Create database schema for sql
|
||||
# each section will represent an own table
|
||||
# Entity, Service, Resolution, Eventmessages
|
||||
# The relation between them is as follows:
|
||||
# one Entity can have many Services
|
||||
|
||||
|
||||
#########################
|
||||
# #
|
||||
# Entity #
|
||||
@@ -163,7 +173,7 @@ class EventmessageBase(BaseModel):
|
||||
..., example=12345
|
||||
) # specific to one group needed to enable visually nested groups
|
||||
msg_type: int = Field(..., example=1) # message type for the label
|
||||
src_did: str = Field(..., example="did:sov:test:2234")
|
||||
src_did: str = Field(..., example="did:sov:test:121")
|
||||
des_did: str = Field(..., example="did:sov:test:120")
|
||||
|
||||
|
||||
@@ -173,6 +183,10 @@ class EventmessageCreate(EventmessageBase):
|
||||
|
||||
class Eventmessage(EventmessageCreate):
|
||||
id: int = Field(...)
|
||||
des_name: Optional[str] = Field(default=None, example="C2")
|
||||
src_name: Optional[str] = Field(default=None, example="C1")
|
||||
msg_type_name: Optional[str] = Field(default=None, example="Request Send")
|
||||
group_name: Optional[str] = Field(default=None, example="Presentation")
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Imports
|
||||
import argparse
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
@@ -10,7 +11,6 @@ from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import Iterator
|
||||
|
||||
# XXX: can we dynamically load this using nix develop?
|
||||
import uvicorn
|
||||
from pydantic import AnyUrl, IPvAnyAddress
|
||||
from pydantic.tools import parse_obj_as
|
||||
@@ -19,9 +19,11 @@ import clan_cli.config as config
|
||||
from clan_cli.emulate_fastapi import apps, get_health
|
||||
from clan_cli.errors import ClanError
|
||||
|
||||
# Setting up logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Function to open the browser for a specified URL
|
||||
def open_browser(base_url: AnyUrl, sub_url: str) -> None:
|
||||
for i in range(5):
|
||||
try:
|
||||
@@ -33,6 +35,7 @@ def open_browser(base_url: AnyUrl, sub_url: str) -> None:
|
||||
_open_browser(url)
|
||||
|
||||
|
||||
# Helper function to open a web browser for a given URL using available browsers
|
||||
def _open_browser(url: AnyUrl) -> subprocess.Popen:
|
||||
for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
|
||||
if shutil.which(browser):
|
||||
@@ -52,6 +55,7 @@ def _open_browser(url: AnyUrl) -> subprocess.Popen:
|
||||
raise ClanError("No browser found")
|
||||
|
||||
|
||||
# Context manager to spawn the Node.js development server
|
||||
@contextmanager
|
||||
def spawn_node_dev_server(host: IPvAnyAddress, port: int) -> Iterator[None]:
|
||||
log.info("Starting node dev server...")
|
||||
@@ -78,6 +82,7 @@ def spawn_node_dev_server(host: IPvAnyAddress, port: int) -> Iterator[None]:
|
||||
proc.terminate()
|
||||
|
||||
|
||||
# Main function to start the server
|
||||
def start_server(args: argparse.Namespace) -> None:
|
||||
with ExitStack() as stack:
|
||||
headers: list[tuple[str, str]] = []
|
||||
@@ -115,6 +120,7 @@ def start_server(args: argparse.Namespace) -> None:
|
||||
sql_models.Base.metadata.drop_all(engine)
|
||||
|
||||
if args.populate:
|
||||
# pre populate the server with some test data
|
||||
test_dir = Path(__file__).parent.parent.parent / "tests"
|
||||
|
||||
if not test_dir.is_dir():
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# Imports
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import asc, func
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql.expression import true
|
||||
|
||||
from ..errors import ClanError
|
||||
from . import schemas, sql_models
|
||||
|
||||
# Functions to manipulate the tables of the database
|
||||
|
||||
|
||||
#########################
|
||||
# #
|
||||
@@ -316,4 +319,11 @@ def create_eventmessage(
|
||||
def get_eventmessages(
|
||||
db: Session, skip: int = 0, limit: int = 100
|
||||
) -> List[sql_models.Eventmessage]:
|
||||
return db.query(sql_models.Eventmessage).offset(skip).limit(limit).all()
|
||||
# Use order_by and desc to sort by timestamp
|
||||
return (
|
||||
db.query(sql_models.Eventmessage)
|
||||
.order_by(asc(sql_models.Eventmessage.timestamp))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Imports
|
||||
from typing import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
@@ -7,6 +8,8 @@ from sqlalchemy.orm import Session, declarative_base, sessionmaker
|
||||
|
||||
URL = "sqlite:///./sql_app.db"
|
||||
|
||||
# Create db engine
|
||||
|
||||
engine = create_engine(URL, connect_args={"check_same_thread": False})
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ from sqlalchemy.orm import relationship
|
||||
from .db_types import Role
|
||||
from .sql_db import Base
|
||||
|
||||
# Relationsship example
|
||||
# https://dev.to/freddiemazzilli/flask-sqlalchemy-relationships-exploring-relationship-associations-igo
|
||||
|
||||
|
||||
# SQLAlchemy model for the "entities" table
|
||||
class Entity(Base):
|
||||
__tablename__ = "entities"
|
||||
|
||||
## Queryable body ##
|
||||
# Primary Key
|
||||
did = Column(String, primary_key=True, index=True)
|
||||
# Indexed Columns
|
||||
name = Column(String, index=True, unique=True)
|
||||
ip = Column(String, index=True)
|
||||
network = Column(String, index=True)
|
||||
@@ -21,69 +21,87 @@ class Entity(Base):
|
||||
stop_health_task = Column(Boolean)
|
||||
|
||||
## Non queryable body ##
|
||||
# In here we deposit: Not yet defined stuff
|
||||
# JSON field for additional non-queryable data
|
||||
other = Column(JSON)
|
||||
|
||||
## Relations ##
|
||||
# One-to-Many relationship with "services" table
|
||||
services = relationship("Service", back_populates="entity")
|
||||
# One-to-Many relationship with "entity_roles" table
|
||||
roles = relationship("EntityRoles", back_populates="entity")
|
||||
# One-to-Many relationship with "service_usage" table
|
||||
consumes = relationship("ServiceUsage", back_populates="consumer_entity")
|
||||
|
||||
|
||||
# SQLAlchemy model for the "entity_roles" table
|
||||
class EntityRoles(Base):
|
||||
__tablename__ = "entity_roles"
|
||||
|
||||
## Queryable body ##
|
||||
# Primary Key
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
# Foreign Key
|
||||
entity_did = Column(String, ForeignKey("entities.did"))
|
||||
# Enum field for role
|
||||
role = Column(Enum(Role), index=True, nullable=False) # type: ignore
|
||||
|
||||
## Relations ##
|
||||
# Many-to-One relationship with "entities" table
|
||||
entity = relationship("Entity", back_populates="roles")
|
||||
|
||||
|
||||
# SQLAlchemy model for the "service_usage" table
|
||||
class ServiceUsage(Base):
|
||||
__tablename__ = "service_usage"
|
||||
|
||||
## Queryable body ##
|
||||
# Primary Key
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
# Foreign Key
|
||||
consumer_entity_did = Column(String, ForeignKey("entities.did"))
|
||||
# Many-to-One relationship with "entities" table
|
||||
consumer_entity = relationship("Entity", back_populates="consumes")
|
||||
times_consumed = Column(Integer, index=True, nullable=False)
|
||||
|
||||
service_uuid = Column(String, ForeignKey("services.uuid"))
|
||||
# Many-to-One relationship with "services" table
|
||||
service = relationship("Service", back_populates="usage")
|
||||
|
||||
|
||||
# SQLAlchemy model for the "services" table
|
||||
class Service(Base):
|
||||
__tablename__ = "services"
|
||||
|
||||
# Queryable body
|
||||
# Primary Key
|
||||
uuid = Column(Text(length=36), primary_key=True, index=True)
|
||||
service_name = Column(String, index=True)
|
||||
service_type = Column(String, index=True)
|
||||
endpoint_url = Column(String, index=True)
|
||||
|
||||
## Non queryable body ##
|
||||
# In here we deposit: Action
|
||||
# JSON fields for additional non-queryable data
|
||||
other = Column(JSON)
|
||||
status = Column(JSON, index=True)
|
||||
action = Column(JSON, index=True)
|
||||
|
||||
## Relations ##
|
||||
# One entity can have many services
|
||||
# One-to-Many relationship with "entities" table
|
||||
entity = relationship("Entity", back_populates="services")
|
||||
entity_did = Column(String, ForeignKey("entities.did"))
|
||||
|
||||
# One-to-Many relationship with "service_usage" table
|
||||
usage = relationship("ServiceUsage", back_populates="service")
|
||||
|
||||
|
||||
# SQLAlchemy model for the "eventmessages" table
|
||||
class Eventmessage(Base):
|
||||
__tablename__ = "eventmessages"
|
||||
|
||||
## Queryable body ##
|
||||
# Primary Key
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
timestamp = Column(Integer, unique=True, index=True)
|
||||
timestamp = Column(Integer, index=True)
|
||||
group = Column(Integer, index=True)
|
||||
group_id = Column(Integer, index=True)
|
||||
msg_type = Column(Integer, index=True) # message type for the label
|
||||
@@ -91,8 +109,10 @@ class Eventmessage(Base):
|
||||
des_did = Column(String, index=True)
|
||||
|
||||
## Non queryable body ##
|
||||
# In here we deposit: Network, Roles, Visible, etc.
|
||||
# JSON field for additional non-queryable data
|
||||
msg = Column(JSON)
|
||||
|
||||
## Relations ##
|
||||
# One-to-Many relationship with "entities" table
|
||||
# One entity can send many messages
|
||||
# (Note: The comment is incomplete and can be extended based on the relationship)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{ age
|
||||
, lib
|
||||
{ lib
|
||||
, argcomplete
|
||||
, fastapi
|
||||
, uvicorn
|
||||
@@ -15,23 +14,17 @@
|
||||
, python3
|
||||
, runCommand
|
||||
, setuptools
|
||||
, sops
|
||||
, stdenv
|
||||
, wheel
|
||||
, fakeroot
|
||||
, rsync
|
||||
, ui-assets
|
||||
, bash
|
||||
, sshpass
|
||||
, zbar
|
||||
, tor
|
||||
, git
|
||||
, nixpkgs
|
||||
, makeDesktopItem
|
||||
, copyDesktopItems
|
||||
, qemu
|
||||
, gnupg
|
||||
, e2fsprogs
|
||||
, mypy
|
||||
, sqlalchemy
|
||||
, websockets
|
||||
@@ -73,17 +66,9 @@ let
|
||||
bash
|
||||
nix
|
||||
fakeroot
|
||||
openssh
|
||||
sshpass
|
||||
zbar
|
||||
tor
|
||||
age
|
||||
rsync
|
||||
sops
|
||||
git
|
||||
mypy
|
||||
qemu
|
||||
e2fsprogs
|
||||
];
|
||||
|
||||
runtimeDependenciesAsSet = builtins.listToAttrs (builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies);
|
||||
|
||||
@@ -12,6 +12,19 @@
|
||||
};
|
||||
inherit (self'.packages.clan-cli) clan-openapi;
|
||||
default = self'.packages.clan-cli;
|
||||
|
||||
|
||||
clan-docker = pkgs.dockerTools.buildImage {
|
||||
name = "clan-docker";
|
||||
tag = "latest";
|
||||
created = "now";
|
||||
config = {
|
||||
Cmd = [ "${self'.packages.clan-cli}/bin/clan" "webui" "--no-open" "--host" "0.0.0.0" ];
|
||||
ExposedPorts = {
|
||||
"2979/tcp" = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
checks = self'.packages.clan-cli.tests;
|
||||
|
||||
9
pkgs/clan-cli/push_docker.sh
Executable file
9
pkgs/clan-cli/push_docker.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck shell=bash
|
||||
set -euo pipefail
|
||||
|
||||
docker login git.tu-berlin.de:5000
|
||||
docker load < result
|
||||
docker image tag clan-docker:latest git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
|
||||
docker image push git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
|
||||
@@ -42,134 +42,6 @@ class DefaultApi:
|
||||
api_client = ApiClient.get_default()
|
||||
self.api_client = api_client
|
||||
|
||||
@validate_arguments
|
||||
def get(self, **kwargs) -> None: # noqa: E501
|
||||
"""Get # noqa: E501
|
||||
|
||||
This method makes a synchronous HTTP request by default. To make an
|
||||
asynchronous HTTP request, please pass async_req=True
|
||||
|
||||
>>> thread = api.get(async_req=True)
|
||||
>>> result = thread.get()
|
||||
|
||||
:param async_req: Whether to execute the request asynchronously.
|
||||
:type async_req: bool, optional
|
||||
:param _request_timeout: timeout setting for this request.
|
||||
If one number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:return: Returns the result object.
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
:rtype: None
|
||||
"""
|
||||
kwargs['_return_http_data_only'] = True
|
||||
if '_preload_content' in kwargs:
|
||||
message = "Error! Please call the get_with_http_info method with `_preload_content` instead and obtain raw data from ApiResponse.raw_data" # noqa: E501
|
||||
raise ValueError(message)
|
||||
return self.get_with_http_info(**kwargs) # noqa: E501
|
||||
|
||||
@validate_arguments
|
||||
def get_with_http_info(self, **kwargs) -> ApiResponse: # noqa: E501
|
||||
"""Get # noqa: E501
|
||||
|
||||
This method makes a synchronous HTTP request by default. To make an
|
||||
asynchronous HTTP request, please pass async_req=True
|
||||
|
||||
>>> thread = api.get_with_http_info(async_req=True)
|
||||
>>> result = thread.get()
|
||||
|
||||
:param async_req: Whether to execute the request asynchronously.
|
||||
:type async_req: bool, optional
|
||||
:param _preload_content: if False, the ApiResponse.data will
|
||||
be set to none and raw_data will store the
|
||||
HTTP response body without reading/decoding.
|
||||
Default is True.
|
||||
:type _preload_content: bool, optional
|
||||
:param _return_http_data_only: response data instead of ApiResponse
|
||||
object with status code, headers, etc
|
||||
:type _return_http_data_only: bool, optional
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param _request_auth: set to override the auth_settings for an a single
|
||||
request; this effectively ignores the authentication
|
||||
in the spec for a single request.
|
||||
:type _request_auth: dict, optional
|
||||
:type _content_type: string, optional: force content-type for the request
|
||||
:return: Returns the result object.
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
_params = locals()
|
||||
|
||||
_all_params = [
|
||||
]
|
||||
_all_params.extend(
|
||||
[
|
||||
'async_req',
|
||||
'_return_http_data_only',
|
||||
'_preload_content',
|
||||
'_request_timeout',
|
||||
'_request_auth',
|
||||
'_content_type',
|
||||
'_headers'
|
||||
]
|
||||
)
|
||||
|
||||
# validate the arguments
|
||||
for _key, _val in _params['kwargs'].items():
|
||||
if _key not in _all_params:
|
||||
raise ApiTypeError(
|
||||
"Got an unexpected keyword argument '%s'"
|
||||
" to method get" % _key
|
||||
)
|
||||
_params[_key] = _val
|
||||
del _params['kwargs']
|
||||
|
||||
_collection_formats = {}
|
||||
|
||||
# process the path parameters
|
||||
_path_params = {}
|
||||
|
||||
# process the query parameters
|
||||
_query_params = []
|
||||
# process the header parameters
|
||||
_header_params = dict(_params.get('_headers', {}))
|
||||
# process the form parameters
|
||||
_form_params = []
|
||||
_files = {}
|
||||
# process the body parameter
|
||||
_body_params = None
|
||||
# set the HTTP header `Accept`
|
||||
_header_params['Accept'] = self.api_client.select_header_accept(
|
||||
['application/json']) # noqa: E501
|
||||
|
||||
# authentication setting
|
||||
_auth_settings = [] # noqa: E501
|
||||
|
||||
_response_types_map = {}
|
||||
|
||||
return self.api_client.call_api(
|
||||
'/ws2_example', 'GET',
|
||||
_path_params,
|
||||
_query_params,
|
||||
_header_params,
|
||||
body=_body_params,
|
||||
post_params=_form_params,
|
||||
files=_files,
|
||||
response_types_map=_response_types_map,
|
||||
auth_settings=_auth_settings,
|
||||
async_req=_params.get('async_req'),
|
||||
_return_http_data_only=_params.get('_return_http_data_only'), # noqa: E501
|
||||
_preload_content=_params.get('_preload_content', True),
|
||||
_request_timeout=_params.get('_request_timeout'),
|
||||
collection_formats=_collection_formats,
|
||||
_request_auth=_params.get('_request_auth'))
|
||||
|
||||
@validate_arguments
|
||||
def get_emulated_enpoints(self, **kwargs) -> str: # noqa: E501
|
||||
"""Get Emulated Enpoints # noqa: E501
|
||||
|
||||
@@ -20,7 +20,7 @@ from pydantic import validate_arguments, ValidationError
|
||||
|
||||
from pydantic import StrictInt
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from openapi_client.models.eventmessage import Eventmessage
|
||||
from openapi_client.models.eventmessage_create import EventmessageCreate
|
||||
@@ -192,7 +192,7 @@ class EventmessagesApi:
|
||||
_request_auth=_params.get('_request_auth'))
|
||||
|
||||
@validate_arguments
|
||||
def get_all_eventmessages(self, skip : Optional[StrictInt] = None, limit : Optional[StrictInt] = None, **kwargs) -> List[Eventmessage]: # noqa: E501
|
||||
def get_all_eventmessages(self, skip : Optional[StrictInt] = None, limit : Optional[StrictInt] = None, **kwargs) -> None: # noqa: E501
|
||||
"""Get All Eventmessages # noqa: E501
|
||||
|
||||
This method makes a synchronous HTTP request by default. To make an
|
||||
@@ -214,7 +214,7 @@ class EventmessagesApi:
|
||||
:return: Returns the result object.
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
:rtype: List[Eventmessage]
|
||||
:rtype: None
|
||||
"""
|
||||
kwargs['_return_http_data_only'] = True
|
||||
if '_preload_content' in kwargs:
|
||||
@@ -258,7 +258,7 @@ class EventmessagesApi:
|
||||
:return: Returns the result object.
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
:rtype: tuple(List[Eventmessage], status_code(int), headers(HTTPHeaderDict))
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
_params = locals()
|
||||
@@ -316,10 +316,7 @@ class EventmessagesApi:
|
||||
# authentication setting
|
||||
_auth_settings = [] # noqa: E501
|
||||
|
||||
_response_types_map = {
|
||||
'200': "List[Eventmessage]",
|
||||
'422': "HTTPValidationError",
|
||||
}
|
||||
_response_types_map = {}
|
||||
|
||||
return self.api_client.call_api(
|
||||
'/api/v1/event_messages', 'GET',
|
||||
|
||||
@@ -1258,3 +1258,157 @@ class ServicesApi:
|
||||
_request_timeout=_params.get('_request_timeout'),
|
||||
collection_formats=_collection_formats,
|
||||
_request_auth=_params.get('_request_auth'))
|
||||
|
||||
@validate_arguments
|
||||
def update_service(self, service_create : ServiceCreate, uuid : Optional[StrictStr] = None, **kwargs) -> Service: # noqa: E501
|
||||
"""Update Service # noqa: E501
|
||||
|
||||
This method makes a synchronous HTTP request by default. To make an
|
||||
asynchronous HTTP request, please pass async_req=True
|
||||
|
||||
>>> thread = api.update_service(service_create, uuid, async_req=True)
|
||||
>>> result = thread.get()
|
||||
|
||||
:param service_create: (required)
|
||||
:type service_create: ServiceCreate
|
||||
:param uuid:
|
||||
:type uuid: str
|
||||
:param async_req: Whether to execute the request asynchronously.
|
||||
:type async_req: bool, optional
|
||||
:param _request_timeout: timeout setting for this request.
|
||||
If one number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:return: Returns the result object.
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
:rtype: Service
|
||||
"""
|
||||
kwargs['_return_http_data_only'] = True
|
||||
if '_preload_content' in kwargs:
|
||||
message = "Error! Please call the update_service_with_http_info method with `_preload_content` instead and obtain raw data from ApiResponse.raw_data" # noqa: E501
|
||||
raise ValueError(message)
|
||||
return self.update_service_with_http_info(service_create, uuid, **kwargs) # noqa: E501
|
||||
|
||||
@validate_arguments
|
||||
def update_service_with_http_info(self, service_create : ServiceCreate, uuid : Optional[StrictStr] = None, **kwargs) -> ApiResponse: # noqa: E501
|
||||
"""Update Service # noqa: E501
|
||||
|
||||
This method makes a synchronous HTTP request by default. To make an
|
||||
asynchronous HTTP request, please pass async_req=True
|
||||
|
||||
>>> thread = api.update_service_with_http_info(service_create, uuid, async_req=True)
|
||||
>>> result = thread.get()
|
||||
|
||||
:param service_create: (required)
|
||||
:type service_create: ServiceCreate
|
||||
:param uuid:
|
||||
:type uuid: str
|
||||
:param async_req: Whether to execute the request asynchronously.
|
||||
:type async_req: bool, optional
|
||||
:param _preload_content: if False, the ApiResponse.data will
|
||||
be set to none and raw_data will store the
|
||||
HTTP response body without reading/decoding.
|
||||
Default is True.
|
||||
:type _preload_content: bool, optional
|
||||
:param _return_http_data_only: response data instead of ApiResponse
|
||||
object with status code, headers, etc
|
||||
:type _return_http_data_only: bool, optional
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param _request_auth: set to override the auth_settings for an a single
|
||||
request; this effectively ignores the authentication
|
||||
in the spec for a single request.
|
||||
:type _request_auth: dict, optional
|
||||
:type _content_type: string, optional: force content-type for the request
|
||||
:return: Returns the result object.
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
:rtype: tuple(Service, status_code(int), headers(HTTPHeaderDict))
|
||||
"""
|
||||
|
||||
_params = locals()
|
||||
|
||||
_all_params = [
|
||||
'service_create',
|
||||
'uuid'
|
||||
]
|
||||
_all_params.extend(
|
||||
[
|
||||
'async_req',
|
||||
'_return_http_data_only',
|
||||
'_preload_content',
|
||||
'_request_timeout',
|
||||
'_request_auth',
|
||||
'_content_type',
|
||||
'_headers'
|
||||
]
|
||||
)
|
||||
|
||||
# validate the arguments
|
||||
for _key, _val in _params['kwargs'].items():
|
||||
if _key not in _all_params:
|
||||
raise ApiTypeError(
|
||||
"Got an unexpected keyword argument '%s'"
|
||||
" to method update_service" % _key
|
||||
)
|
||||
_params[_key] = _val
|
||||
del _params['kwargs']
|
||||
|
||||
_collection_formats = {}
|
||||
|
||||
# process the path parameters
|
||||
_path_params = {}
|
||||
|
||||
# process the query parameters
|
||||
_query_params = []
|
||||
if _params.get('uuid') is not None: # noqa: E501
|
||||
_query_params.append(('uuid', _params['uuid']))
|
||||
|
||||
# process the header parameters
|
||||
_header_params = dict(_params.get('_headers', {}))
|
||||
# process the form parameters
|
||||
_form_params = []
|
||||
_files = {}
|
||||
# process the body parameter
|
||||
_body_params = None
|
||||
if _params['service_create'] is not None:
|
||||
_body_params = _params['service_create']
|
||||
|
||||
# set the HTTP header `Accept`
|
||||
_header_params['Accept'] = self.api_client.select_header_accept(
|
||||
['application/json']) # noqa: E501
|
||||
|
||||
# set the HTTP header `Content-Type`
|
||||
_content_types_list = _params.get('_content_type',
|
||||
self.api_client.select_header_content_type(
|
||||
['application/json']))
|
||||
if _content_types_list:
|
||||
_header_params['Content-Type'] = _content_types_list
|
||||
|
||||
# authentication setting
|
||||
_auth_settings = [] # noqa: E501
|
||||
|
||||
_response_types_map = {
|
||||
'200': "Service",
|
||||
'422': "HTTPValidationError",
|
||||
}
|
||||
|
||||
return self.api_client.call_api(
|
||||
'/api/v1/service', 'PUT',
|
||||
_path_params,
|
||||
_query_params,
|
||||
_header_params,
|
||||
body=_body_params,
|
||||
post_params=_form_params,
|
||||
files=_files,
|
||||
response_types_map=_response_types_map,
|
||||
auth_settings=_auth_settings,
|
||||
async_req=_params.get('async_req'),
|
||||
_return_http_data_only=_params.get('_return_http_data_only'), # noqa: E501
|
||||
_preload_content=_params.get('_preload_content', True),
|
||||
_request_timeout=_params.get('_request_timeout'),
|
||||
collection_formats=_collection_formats,
|
||||
_request_auth=_params.get('_request_auth'))
|
||||
|
||||
@@ -4,70 +4,11 @@ All URIs are relative to *http://localhost*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**get**](DefaultApi.md#get) | **GET** /ws2_example | Get
|
||||
[**get_emulated_enpoints**](DefaultApi.md#get_emulated_enpoints) | **GET** /emulate | Get Emulated Enpoints
|
||||
[**health**](DefaultApi.md#health) | **GET** /health | Health
|
||||
[**root**](DefaultApi.md#root) | **GET** /{path_name} | Root
|
||||
|
||||
|
||||
# **get**
|
||||
> get()
|
||||
|
||||
Get
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
import time
|
||||
import os
|
||||
import openapi_client
|
||||
from openapi_client.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to http://localhost
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = openapi_client.Configuration(
|
||||
host = "http://localhost"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
with openapi_client.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = openapi_client.DefaultApi(api_client)
|
||||
|
||||
try:
|
||||
# Get
|
||||
api_instance.get()
|
||||
except Exception as e:
|
||||
print("Exception when calling DefaultApi->get: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | Successful Response | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **get_emulated_enpoints**
|
||||
> str get_emulated_enpoints()
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ Name | Type | Description | Notes
|
||||
**des_did** | **str** | |
|
||||
**msg** | **object** | |
|
||||
**id** | **int** | |
|
||||
**des_name** | **str** | | [optional]
|
||||
**src_name** | **str** | | [optional]
|
||||
**msg_type_name** | **str** | | [optional]
|
||||
**group_name** | **str** | | [optional]
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ No authorization required
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **get_all_eventmessages**
|
||||
> List[Eventmessage] get_all_eventmessages(skip=skip, limit=limit)
|
||||
> get_all_eventmessages(skip=skip, limit=limit)
|
||||
|
||||
Get All Eventmessages
|
||||
|
||||
@@ -86,7 +86,6 @@ Get All Eventmessages
|
||||
import time
|
||||
import os
|
||||
import openapi_client
|
||||
from openapi_client.models.eventmessage import Eventmessage
|
||||
from openapi_client.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
@@ -106,9 +105,7 @@ with openapi_client.ApiClient(configuration) as api_client:
|
||||
|
||||
try:
|
||||
# Get All Eventmessages
|
||||
api_response = api_instance.get_all_eventmessages(skip=skip, limit=limit)
|
||||
print("The response of EventmessagesApi->get_all_eventmessages:\n")
|
||||
pprint(api_response)
|
||||
api_instance.get_all_eventmessages(skip=skip, limit=limit)
|
||||
except Exception as e:
|
||||
print("Exception when calling EventmessagesApi->get_all_eventmessages: %s\n" % e)
|
||||
```
|
||||
@@ -124,7 +121,7 @@ Name | Type | Description | Notes
|
||||
|
||||
### Return type
|
||||
|
||||
[**List[Eventmessage]**](Eventmessage.md)
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Method | HTTP request | Description
|
||||
[**get_service_by_uuid**](ServicesApi.md#get_service_by_uuid) | **GET** /api/v1/service | Get Service By Uuid
|
||||
[**get_services_without_entity**](ServicesApi.md#get_services_without_entity) | **GET** /api/v1/services_without_entity | Get Services Without Entity
|
||||
[**inc_service_usage**](ServicesApi.md#inc_service_usage) | **PUT** /api/v1/inc_service_usage | Inc Service Usage
|
||||
[**update_service**](ServicesApi.md#update_service) | **PUT** /api/v1/service | Update Service
|
||||
|
||||
|
||||
# **add_service_usage**
|
||||
@@ -564,3 +565,72 @@ No authorization required
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **update_service**
|
||||
> Service update_service(service_create, uuid=uuid)
|
||||
|
||||
Update Service
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
import time
|
||||
import os
|
||||
import openapi_client
|
||||
from openapi_client.models.service import Service
|
||||
from openapi_client.models.service_create import ServiceCreate
|
||||
from openapi_client.rest import ApiException
|
||||
from pprint import pprint
|
||||
|
||||
# Defining the host is optional and defaults to http://localhost
|
||||
# See configuration.py for a list of all supported configuration parameters.
|
||||
configuration = openapi_client.Configuration(
|
||||
host = "http://localhost"
|
||||
)
|
||||
|
||||
|
||||
# Enter a context with an instance of the API client
|
||||
with openapi_client.ApiClient(configuration) as api_client:
|
||||
# Create an instance of the API class
|
||||
api_instance = openapi_client.ServicesApi(api_client)
|
||||
service_create = openapi_client.ServiceCreate() # ServiceCreate |
|
||||
uuid = 'bdd640fb-0667-1ad1-1c80-317fa3b1799d' # str | (optional) (default to 'bdd640fb-0667-1ad1-1c80-317fa3b1799d')
|
||||
|
||||
try:
|
||||
# Update Service
|
||||
api_response = api_instance.update_service(service_create, uuid=uuid)
|
||||
print("The response of ServicesApi->update_service:\n")
|
||||
pprint(api_response)
|
||||
except Exception as e:
|
||||
print("Exception when calling ServicesApi->update_service: %s\n" % e)
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**service_create** | [**ServiceCreate**](ServiceCreate.md)| |
|
||||
**uuid** | **str**| | [optional] [default to 'bdd640fb-0667-1ad1-1c80-317fa3b1799d']
|
||||
|
||||
### Return type
|
||||
|
||||
[**Service**](Service.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
### HTTP response details
|
||||
| Status code | Description | Response headers |
|
||||
|-------------|-------------|------------------|
|
||||
**200** | Successful Response | - |
|
||||
**422** | Validation Error | - |
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import re # noqa: F401
|
||||
import json
|
||||
|
||||
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
from pydantic import BaseModel, Field, StrictInt, StrictStr
|
||||
|
||||
class Eventmessage(BaseModel):
|
||||
@@ -33,7 +33,11 @@ class Eventmessage(BaseModel):
|
||||
des_did: StrictStr = Field(...)
|
||||
msg: Dict[str, Any] = Field(...)
|
||||
id: StrictInt = Field(...)
|
||||
__properties = ["timestamp", "group", "group_id", "msg_type", "src_did", "des_did", "msg", "id"]
|
||||
des_name: Optional[StrictStr] = None
|
||||
src_name: Optional[StrictStr] = None
|
||||
msg_type_name: Optional[StrictStr] = None
|
||||
group_name: Optional[StrictStr] = None
|
||||
__properties = ["timestamp", "group", "group_id", "msg_type", "src_did", "des_did", "msg", "id", "des_name", "src_name", "msg_type_name", "group_name"]
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration"""
|
||||
@@ -78,7 +82,11 @@ class Eventmessage(BaseModel):
|
||||
"src_did": obj.get("src_did"),
|
||||
"des_did": obj.get("des_did"),
|
||||
"msg": obj.get("msg"),
|
||||
"id": obj.get("id")
|
||||
"id": obj.get("id"),
|
||||
"des_name": obj.get("des_name"),
|
||||
"src_name": obj.get("src_name"),
|
||||
"msg_type_name": obj.get("msg_type_name"),
|
||||
"group_name": obj.get("group_name")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ random.seed(42)
|
||||
host = config.host
|
||||
port_dlg = config.port_dlg
|
||||
port_ap = config.port_ap
|
||||
port_client_base = config.port_client_base
|
||||
port_client_base = config._port_client_base
|
||||
|
||||
num_uuids = 100
|
||||
uuids = [str(uuid.UUID(int=random.getrandbits(128))) for i in range(num_uuids)]
|
||||
@@ -38,9 +38,9 @@ def test_health(api_client: ApiClient) -> None:
|
||||
assert res.status == Status.ONLINE
|
||||
|
||||
|
||||
def create_entities(num: int = 10, role: str = "entity") -> list[EntityCreate]:
|
||||
def create_entities(num: int = 5, role: str = "entity") -> list[EntityCreate]:
|
||||
res = []
|
||||
for i in range(num):
|
||||
for i in range(1, num + 1):
|
||||
en = EntityCreate(
|
||||
did=f"did:sov:test:12{i}",
|
||||
name=f"C{i}",
|
||||
@@ -75,21 +75,28 @@ def create_entities(num: int = 10, role: str = "entity") -> list[EntityCreate]:
|
||||
|
||||
|
||||
def create_service(idx: int, entity: Entity) -> ServiceCreate:
|
||||
idx += 1
|
||||
se = ServiceCreate(
|
||||
uuid=uuids[idx],
|
||||
service_name=f"Carlos Printing{idx}",
|
||||
service_type="3D Printing",
|
||||
endpoint_url=f"{entity.ip}/v1/print_daemon{idx}",
|
||||
endpoint_url=f"http://{entity.ip}/v1/print_daemon{idx}",
|
||||
status={"data": ["draft", "registered"]},
|
||||
other={},
|
||||
action={
|
||||
"data": [
|
||||
{"name": "register", "path": "/register"},
|
||||
{"name": "deregister", "path": "/deregister"},
|
||||
{
|
||||
"name": "register",
|
||||
"endpoint": f"http://{entity.ip}/v1/print_daemon{idx}/register",
|
||||
},
|
||||
{
|
||||
"name": "deregister",
|
||||
"endpoint": f"http://{entity.ip}/v1/print_daemon{idx}/deregister",
|
||||
},
|
||||
]
|
||||
},
|
||||
entity_did=entity.did,
|
||||
usage=[],
|
||||
usage=[{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
|
||||
)
|
||||
|
||||
return se
|
||||
@@ -107,59 +114,58 @@ def test_create_entities(api_client: ApiClient) -> None:
|
||||
def test_create_services(api_client: ApiClient) -> None:
|
||||
sapi = ServicesApi(api_client=api_client)
|
||||
eapi = EntitiesApi(api_client=api_client)
|
||||
for midx, entity in enumerate(eapi.get_all_entities()):
|
||||
for idx in range(4):
|
||||
service_obj = create_service(idx + 4 * midx, entity)
|
||||
service = sapi.create_service(service_obj)
|
||||
assert service.uuid == service_obj.uuid
|
||||
for midx, entity in enumerate(eapi.get_entity_by_roles([Role("service_prosumer")])):
|
||||
service_obj = create_service(midx, entity)
|
||||
service = sapi.create_service(service_obj)
|
||||
assert service.uuid == service_obj.uuid
|
||||
|
||||
|
||||
random.seed(77)
|
||||
|
||||
|
||||
def create_eventmessages(num: int = 2) -> list[EventmessageCreate]:
|
||||
def create_eventmessages(num: int = 4) -> list[EventmessageCreate]:
|
||||
res = []
|
||||
starttime = int(time.time())
|
||||
for i in range(num):
|
||||
group_id = i % 5 + random.getrandbits(6)
|
||||
for i2 in range(1, num + 1):
|
||||
group_id = i2 % 5 + random.getrandbits(6) + 1
|
||||
em_req_send = EventmessageCreate(
|
||||
timestamp=starttime + i * 10,
|
||||
group=i % 5,
|
||||
timestamp=starttime + i2 * 10,
|
||||
group=i2 % 5,
|
||||
group_id=group_id,
|
||||
msg_type=1,
|
||||
src_did=f"did:sov:test:12{i}",
|
||||
des_did=f"did:sov:test:12{i+1}",
|
||||
src_did=f"did:sov:test:12{i2}",
|
||||
des_did=f"did:sov:test:12{i2+1}",
|
||||
msg={},
|
||||
)
|
||||
res.append(em_req_send)
|
||||
em_req_rec = EventmessageCreate(
|
||||
timestamp=starttime + (i * 10) + 2,
|
||||
group=i % 5,
|
||||
timestamp=starttime + (i2 * 10) + 2,
|
||||
group=i2 % 5,
|
||||
group_id=group_id,
|
||||
msg_type=2,
|
||||
src_did=f"did:sov:test:12{i}",
|
||||
des_did=f"did:sov:test:12{i+1}",
|
||||
src_did=f"did:sov:test:12{i2}",
|
||||
des_did=f"did:sov:test:12{i2+1}",
|
||||
msg={},
|
||||
)
|
||||
res.append(em_req_rec)
|
||||
group_id = i % 5 + random.getrandbits(6)
|
||||
group_id = i2 % 5 + random.getrandbits(6)
|
||||
em_res_send = EventmessageCreate(
|
||||
timestamp=starttime + i * 10 + 4,
|
||||
group=i % 5,
|
||||
timestamp=starttime + i2 * 10 + 4,
|
||||
group=i2 % 5,
|
||||
group_id=group_id,
|
||||
msg_type=3,
|
||||
src_did=f"did:sov:test:12{i+1}",
|
||||
des_did=f"did:sov:test:12{i}",
|
||||
src_did=f"did:sov:test:12{i2+1}",
|
||||
des_did=f"did:sov:test:12{i2}",
|
||||
msg={},
|
||||
)
|
||||
res.append(em_res_send)
|
||||
em_res_rec = EventmessageCreate(
|
||||
timestamp=starttime + (i * 10) + 8,
|
||||
group=i % 5,
|
||||
timestamp=starttime + (i2 * 10) + 8,
|
||||
group=i2 % 5,
|
||||
group_id=group_id,
|
||||
msg_type=4,
|
||||
src_did=f"did:sov:test:12{i+1}",
|
||||
des_did=f"did:sov:test:12{i}",
|
||||
src_did=f"did:sov:test:12{i2+1}",
|
||||
des_did=f"did:sov:test:12{i2}",
|
||||
msg={},
|
||||
)
|
||||
res.append(em_res_rec)
|
||||
@@ -168,11 +174,12 @@ def create_eventmessages(num: int = 2) -> list[EventmessageCreate]:
|
||||
|
||||
def test_create_eventmessages(api_client: ApiClient) -> None:
|
||||
api = EventmessagesApi(api_client=api_client)
|
||||
assert [] == api.get_all_eventmessages()
|
||||
|
||||
assert api.get_all_eventmessages() is None
|
||||
for idx, own_eventmsg in enumerate(create_eventmessages()):
|
||||
res: Eventmessage = api.create_eventmessage(own_eventmsg)
|
||||
|
||||
assert res.msg == own_eventmsg.msg
|
||||
assert res.src_did == own_eventmsg.src_did
|
||||
assert res.des_did == own_eventmsg.des_did
|
||||
assert [] != api.get_all_eventmessages()
|
||||
assert {} != api.get_all_eventmessages()
|
||||
|
||||
48
pkgs/clan-cli/upload_ui_assets.sh
Executable file
48
pkgs/clan-cli/upload_ui_assets.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck shell=bash
|
||||
set -euo pipefail
|
||||
|
||||
# GITLAB_TOKEN
|
||||
if [[ -z "${GITLAB_TOKEN:-}" ]]; then
|
||||
cat <<EOF
|
||||
GITLAB_TOKEN environment var is not set. Please generate a new token under
|
||||
https://git.tu-berlin.de/internet-of-services-lab/service-aware-network-front-end/-/settings/access_tokens
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
cleanup() { rm -rf "$tmpdir"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create a new ui build
|
||||
nix build '.#ui' --out-link "$tmpdir/result"
|
||||
|
||||
|
||||
tar --transform 's,^\.,assets,' -czvf "$tmpdir/assets.tar.gz" -C "$tmpdir"/result/lib/node_modules/*/out .
|
||||
# upload ui assets to gitlab
|
||||
gitlab_base="https://git.tu-berlin.de/api/v4/projects/internet-of-services-lab%2Fservice-aware-network-front-end"
|
||||
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
||||
--upload-file "$tmpdir/assets.tar.gz" \
|
||||
"$gitlab_base/packages/generic/ui-assets/1.0.0/ui-assets.tar.gz"
|
||||
|
||||
|
||||
# write url and hash to ui-assets.nix
|
||||
url="$gitlab_base/packages/generic/ui-assets/1.0.0/ui-assets.tar.gz"
|
||||
PROJECT_DIR=$(git rev-parse --show-toplevel)
|
||||
cat > "$PROJECT_DIR/pkgs/ui/nix/ui-assets.nix" <<EOF
|
||||
{ fetchzip }:
|
||||
fetchzip {
|
||||
url = "$url";
|
||||
sha256 = "$(nix-prefetch-url --unpack $url)";
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
cat <<EOF
|
||||
Please commit the changes to ui-assets.nix and push them to the repository.
|
||||
If you want clan webui to use the new ui assets.
|
||||
$ git commit -m "Update ui-assets.nix" "$PROJECT_DIR/pkgs/ui/nix/ui-assets.nix"
|
||||
$ git push
|
||||
EOF
|
||||
@@ -1,10 +1,19 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:tailwindcss/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"ignorePatterns": ["**/src/api/*"],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"**/src/api/*"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"tailwindcss/no-custom-classname": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,40 @@
|
||||
# cLan - awesome UI
|
||||
# Web UI
|
||||
|
||||
**Dependency Management**: We use the [Nix package manager](https://nixos.org/) to manage dependencies and ensure reproducibility, making your development process more robust.
|
||||
|
||||
The files in `src/api` are autogenerated from the openapi.json. The openapi.json comes from the backend, from fastapi which autogenerates the openapi.json file from the python code.
|
||||
|
||||
We then use orval to generate typescript files to `src/api`
|
||||
|
||||
## Build Development Web UI
|
||||
|
||||
To build the dev web ui with hotreloading
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Build release Web UI
|
||||
|
||||
To build the release version execute
|
||||
|
||||
```bash
|
||||
nix build .#ui
|
||||
```
|
||||
|
||||
you can find the output in your current working directory at a symlink called `result`
|
||||
The actual web files served by the web server are located in
|
||||
`result/lib/node_modules/clan-ui/out`
|
||||
|
||||
You can also build it directly in you dev environment. Notice however that this will clutter your directory. The out directory is directly in you working dir.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Development environment
|
||||
|
||||
The development environment created by `nix develop` or automatically by `direnv` is located at [shell.nix](shell.nix). The `shellHook` variable executes bash code.
|
||||
|
||||
## Updating dependencies
|
||||
|
||||
@@ -15,15 +51,3 @@ The prettier tailwind class sorting is not yet working properly with our devShel
|
||||
To sort classnames manually:
|
||||
|
||||
`cd /clan-core/pkgs/ui/`
|
||||
|
||||
## Upload ui to gitea
|
||||
|
||||
Create a gitea token here: https://git.clan.lol/user/settings/applications
|
||||
|
||||
Than run this command:
|
||||
|
||||
```
|
||||
GITEA_TOKEN=<YOUR_TOKEN> nix run .#update-ui-assets
|
||||
```
|
||||
|
||||
.
|
||||
|
||||
1
pkgs/ui/_document.js
Normal file
1
pkgs/ui/_document.js
Normal file
@@ -0,0 +1 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>;
|
||||
@@ -5,6 +5,12 @@ const nextConfig = {
|
||||
eslint: {
|
||||
dirs: ["src"],
|
||||
},
|
||||
generateBuildId: async () => {
|
||||
// This could be anything, using the latest git hash
|
||||
return process.env.NEXT_BUILD_ID;
|
||||
},
|
||||
outputFileTracing: true,
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -70,6 +70,7 @@ in
|
||||
# nextjs chaches some stuff in $HOME
|
||||
built.override.preBuild = ''
|
||||
export HOME=./home
|
||||
export NEXT_BUILD_ID=$(git log -1 --pretty=format:"%H")
|
||||
|
||||
|
||||
echo "----------- GENERATE API TS ------------"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{ fetchzip }:
|
||||
fetchzip {
|
||||
url = "https://gitea.gchq.icu/api/packages/IoSL/generic/IoSL-service-aware-frontend/04h6w20fgq1zd4qzqm1rqkyk1s3mxnra1088icdrshsdm29x81xa/assets.tar.gz";
|
||||
sha256 = "04h6w20fgq1zd4qzqm1rqkyk1s3mxnra1088icdrshsdm29x81xa";
|
||||
url = "https://gitea.gchq.icu/api/packages/IoSL/generic/IoSL-service-aware-frontend/15svaig548jz1l8qsiqcycmw3hkb4805rb08mwlv2isxxshrj9ij/assets.tar.gz";
|
||||
sha256 = "15svaig548jz1l8qsiqcycmw3hkb4805rb08mwlv2isxxshrj9ij";
|
||||
}
|
||||
|
||||
1067
pkgs/ui/package-lock.json
generated
1067
pkgs/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
"axios": "^1.4.0",
|
||||
"classnames": "^2.3.2",
|
||||
"hex-rgb": "^5.0.0",
|
||||
"mermaid": "^10.6.1",
|
||||
"next": "13.4.12",
|
||||
"postcss": "8.4.27",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
|
||||
@@ -8,7 +8,7 @@ pkgs.mkShell {
|
||||
fmod.config.floco.settings.nodePackage
|
||||
];
|
||||
shellHook = ''
|
||||
ID=${pkg.built.tree}
|
||||
export ID=${pkg.built.tree}
|
||||
currID=$(cat .floco/.node_modules_id 2> /dev/null)
|
||||
|
||||
mkdir -p .floco
|
||||
@@ -22,10 +22,11 @@ pkgs.mkShell {
|
||||
ln -sf ${pkgs.roboto}/share/fonts ./src
|
||||
|
||||
export PATH="$PATH:$(realpath ./node_modules)/.bin"
|
||||
|
||||
export NEXT_BUILD_ID=$(git log -1 --pretty=format:"%H")
|
||||
|
||||
# re-generate the api code
|
||||
rm -rf src/api openapi.json
|
||||
rm -f --interactive=never openapi.json
|
||||
rm -rf src/api
|
||||
cp ${clanPkgs.clan-openapi}/openapi.json .
|
||||
orval
|
||||
'';
|
||||
|
||||
@@ -6,13 +6,15 @@ import { useGetAllRepositories } from "@/api/repositories/repositories";
|
||||
import SummaryDetails from "@/components/summary_card";
|
||||
import CustomTable from "@/components/table";
|
||||
import {
|
||||
APSummaryDetails,
|
||||
APAttachmentsTableConfig,
|
||||
APServiceRepositoryTableConfig,
|
||||
} from "@/config/access_point";
|
||||
import { useEffect } from "react";
|
||||
import useGetEntityByNameOrDid from "@/components/hooks/useGetEntityByNameOrDid";
|
||||
import { projectConfig } from "@/config/config";
|
||||
|
||||
export default function AccessPoint() {
|
||||
const { entity } = useGetEntityByNameOrDid("AP");
|
||||
const {
|
||||
data: APAttachementData,
|
||||
isLoading: loadingAttachements,
|
||||
@@ -45,7 +47,7 @@ export default function AccessPoint() {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
onRefresh();
|
||||
}, 5000);
|
||||
}, projectConfig.REFRESH_FREQUENCY);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -54,10 +56,25 @@ export default function AccessPoint() {
|
||||
return (
|
||||
<div className="m-10">
|
||||
<SummaryDetails
|
||||
fake
|
||||
hasRefreshButton
|
||||
onRefresh={onRefresh}
|
||||
entity={{ name: "Access Point", details: APSummaryDetails }}
|
||||
entity={{
|
||||
name: "Access Point",
|
||||
details: [
|
||||
{
|
||||
label: "DID",
|
||||
value: entity?.did,
|
||||
},
|
||||
{
|
||||
label: "IP",
|
||||
value: entity?.ip,
|
||||
},
|
||||
{
|
||||
label: "Network",
|
||||
value: entity?.network,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<h4>Attachment View</h4>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
"use client";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ClientTableConfig, ServiceTableConfig } from "@/config/client_1";
|
||||
import CustomTable from "@/components/table";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Snackbar,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import CopyToClipboard from "@/components/copy_to_clipboard";
|
||||
import {
|
||||
attachEntity,
|
||||
detachEntity,
|
||||
@@ -26,6 +21,10 @@ import useGetEntityByNameOrDid from "@/components/hooks/useGetEntityByNameOrDid"
|
||||
import { useGetAllServices } from "@/api/services/services";
|
||||
import axios from "axios";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import SummaryDetails from "@/components/summary_card";
|
||||
import { projectConfig } from "@/config/config";
|
||||
import ConsumeDisplayComponent from "@/components/consume_content";
|
||||
|
||||
interface SnackMessage {
|
||||
message: string;
|
||||
@@ -105,14 +104,16 @@ const AttachButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default function Client({
|
||||
params,
|
||||
}: {
|
||||
params: { client_name: string };
|
||||
}) {
|
||||
const { client_name } = params;
|
||||
export default function Client() {
|
||||
const searchParams = useSearchParams();
|
||||
const name = searchParams.get("name") ?? "";
|
||||
const [consumeContent, setConsumeContent] = useState(null);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState<
|
||||
SnackMessage | undefined
|
||||
>(undefined);
|
||||
|
||||
const { entity: entity } = useGetEntityByNameOrDid(client_name);
|
||||
const { entity: entity } = useGetEntityByNameOrDid(name);
|
||||
const {
|
||||
data: services,
|
||||
isLoading: services_loading,
|
||||
@@ -137,23 +138,22 @@ export default function Client({
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
onRefresh();
|
||||
}, 5000);
|
||||
}, projectConfig.REFRESH_FREQUENCY);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const cardContentRef = useRef(null);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState<
|
||||
SnackMessage | undefined
|
||||
>(undefined);
|
||||
|
||||
const closeSnackBar = () => {
|
||||
setSnackbarMessage(undefined);
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
// Consume
|
||||
const handleConsumeContent = (content: any) => {
|
||||
setConsumeContent(content);
|
||||
};
|
||||
|
||||
if (services_loading) return <Skeleton height={500} />;
|
||||
if (!services) return <Alert severity="error">Client not found</Alert>;
|
||||
|
||||
@@ -180,34 +180,43 @@ export default function Client({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card variant="outlined">
|
||||
<CardHeader
|
||||
subheader="Summary"
|
||||
action={<CopyToClipboard contentRef={cardContentRef} />}
|
||||
/>
|
||||
<CardContent ref={cardContentRef}>
|
||||
<Typography color="text.primary" gutterBottom>
|
||||
DID: <code>{entity?.did}</code>
|
||||
</Typography>
|
||||
<Typography color="text.primary" gutterBottom>
|
||||
IP: <code>{entity?.ip}</code>
|
||||
</Typography>
|
||||
<Typography color="text.primary" gutterBottom>
|
||||
Network: <code>{entity?.other?.network}</code>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div>
|
||||
<h4>Client View</h4>
|
||||
<CustomTable
|
||||
loading={services_loading}
|
||||
data={clients}
|
||||
configuration={ClientTableConfig}
|
||||
tkey="client-table"
|
||||
/>
|
||||
<SummaryDetails
|
||||
entity={{
|
||||
name: "",
|
||||
details: [
|
||||
{ label: "DID", value: entity?.did },
|
||||
{ label: "IP", value: entity?.ip },
|
||||
{ label: "Network", value: entity?.network },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "nowrap",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ width: consumeContent ? "55%" : "100%" }}>
|
||||
<h4>Service Consumer View</h4>
|
||||
<CustomTable
|
||||
loading={services_loading}
|
||||
data={clients}
|
||||
onConsumeAction={handleConsumeContent}
|
||||
configuration={ClientTableConfig}
|
||||
tkey="client-table"
|
||||
/>
|
||||
</div>
|
||||
{consumeContent && (
|
||||
<div style={{ width: "40%" }}>
|
||||
<h4>Service Output</h4>
|
||||
<ConsumeDisplayComponent htmlContent={consumeContent} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4>Service View</h4>
|
||||
<h4>Service Producer View</h4>
|
||||
<CustomTable
|
||||
loading={services_loading}
|
||||
data={services?.data}
|
||||
5
pkgs/ui/src/app/client/page.tsx
Normal file
5
pkgs/ui/src/app/client/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import Client from "@/app/client/client";
|
||||
|
||||
export default function Page() {
|
||||
return <Client />;
|
||||
}
|
||||
@@ -1,26 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { DLGResolutionTableConfig, DLGSummaryDetails } from "@/config/dlg";
|
||||
import { DLGResolutionTableConfig } from "@/config/dlg";
|
||||
import CustomTable from "@/components/table";
|
||||
import SummaryDetails from "@/components/summary_card";
|
||||
import useFetch from "@/components/hooks/useFetch";
|
||||
import { useEffect } from "react";
|
||||
import { useGetAllResolutions } from "@/api/resolution/resolution";
|
||||
import { mutate } from "swr";
|
||||
import useGetEntityByNameOrDid from "@/components/hooks/useGetEntityByNameOrDid";
|
||||
import { projectConfig } from "@/config/config";
|
||||
|
||||
export default function DLG() {
|
||||
const { entity } = useGetEntityByNameOrDid("DLG");
|
||||
const {
|
||||
data: resolutionData,
|
||||
loading: loadingResolutions,
|
||||
fetch,
|
||||
} = useFetch("/get_resolutions");
|
||||
isLoading: loadingResolutions,
|
||||
swrKey: resolutionsKeyFunc,
|
||||
} = useGetAllResolutions();
|
||||
|
||||
const onRefresh = () => {
|
||||
fetch();
|
||||
const resolutionsKey =
|
||||
typeof resolutionsKeyFunc === "function"
|
||||
? resolutionsKeyFunc()
|
||||
: resolutionsKeyFunc;
|
||||
|
||||
if (resolutionsKey) {
|
||||
mutate(resolutionsKey);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
onRefresh();
|
||||
}, 5000);
|
||||
}, projectConfig.REFRESH_FREQUENCY);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -29,19 +40,31 @@ export default function DLG() {
|
||||
return (
|
||||
<div className="m-10">
|
||||
<SummaryDetails
|
||||
fake
|
||||
hasRefreshButton
|
||||
onRefresh={onRefresh}
|
||||
entity={{
|
||||
name: "Distributed Ledger Gateway",
|
||||
details: DLGSummaryDetails,
|
||||
details: [
|
||||
{
|
||||
label: "DID",
|
||||
value: entity?.did,
|
||||
},
|
||||
{
|
||||
label: "IP",
|
||||
value: entity?.ip,
|
||||
},
|
||||
{
|
||||
label: "Network",
|
||||
value: entity?.network,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<h4>DID Resolution View</h4>
|
||||
<CustomTable
|
||||
loading={loadingResolutions}
|
||||
data={resolutionData}
|
||||
data={resolutionData?.data}
|
||||
configuration={DLGResolutionTableConfig}
|
||||
tkey="resolution_table"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useAppState } from "@/components/hooks/useAppContext";
|
||||
import { NoDataOverlay } from "@/components/noDataOverlay";
|
||||
import SummaryDetails from "@/components/summary_card";
|
||||
import CustomTable from "@/components/table";
|
||||
import { HomeTableConfig } from "@/config/home";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect } from "react";
|
||||
import { mutate } from "swr";
|
||||
import ErrorBoundary from "@/components/error_boundary";
|
||||
import { projectConfig } from "@/config/config";
|
||||
|
||||
const NoSSRSequenceDiagram = dynamic(
|
||||
() => import("../../components/sequence_diagram"),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
const { data } = useAppState();
|
||||
@@ -24,7 +31,7 @@ export default function Home() {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
onRefresh();
|
||||
}, 5000);
|
||||
}, projectConfig.REFRESH_FREQUENCY);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -36,7 +43,6 @@ export default function Home() {
|
||||
entity={{ name: "Home", details: [] }}
|
||||
hasRefreshButton={true}
|
||||
onRefresh={onRefresh}
|
||||
hasAttachDetach={false}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -51,7 +57,9 @@ export default function Home() {
|
||||
|
||||
<div>
|
||||
<h4>Sequence Diagram</h4>
|
||||
<NoDataOverlay label="No Activity yet" />
|
||||
<ErrorBoundary>
|
||||
<NoSSRSequenceDiagram />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CssBaseline,
|
||||
IconButton,
|
||||
ThemeProvider,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import { StyledEngineProvider } from "@mui/material/styles";
|
||||
@@ -50,6 +51,12 @@ export default function RootLayout({
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Service Aware Networks" />
|
||||
<link rel="icon" href="tub-favicon.ico" sizes="any" />
|
||||
<script
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `mermaid.initialize({startOnLoad: true});`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={userPrefersDarkmode ? darkTheme : lightTheme}>
|
||||
@@ -77,13 +84,15 @@ export default function RootLayout({
|
||||
>
|
||||
<div className="grid grid-cols-3">
|
||||
<div className="col-span-1">
|
||||
<IconButton
|
||||
style={{ padding: "12px" }}
|
||||
hidden={true}
|
||||
onClick={() => setShowSidebar((c) => !c)}
|
||||
>
|
||||
{!showSidebar && <MenuIcon />}
|
||||
</IconButton>
|
||||
<Tooltip placement="right" title="Expand Sidebar">
|
||||
<IconButton
|
||||
style={{ padding: "12px" }}
|
||||
hidden={true}
|
||||
onClick={() => setShowSidebar((c) => !c)}
|
||||
>
|
||||
{!showSidebar && <MenuIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
73
pkgs/ui/src/components/consume_action/index.tsx
Normal file
73
pkgs/ui/src/components/consume_action/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Button, CircularProgress, Snackbar } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
const ConsumeAction = ({
|
||||
endpoint,
|
||||
onConsume,
|
||||
}: {
|
||||
endpoint: string;
|
||||
rowData?: any;
|
||||
onConsume?: any;
|
||||
}) => {
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (error) console.error("Error in state", error);
|
||||
|
||||
const handleConsume = () => {
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const axiosConfig = {
|
||||
url: endpoint,
|
||||
method: "GET",
|
||||
data: null,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
axios(axiosConfig)
|
||||
.then((response) => {
|
||||
if (onConsume) {
|
||||
onConsume(response.data);
|
||||
console.log("I got the data from consume: ", response.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (onConsume) onConsume(null);
|
||||
console.error("Error happened during consume: ", error);
|
||||
setError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button disabled={loading} onClick={handleConsume} variant="contained">
|
||||
{loading ? <CircularProgress size={24} /> : `Consume`}
|
||||
</Button>
|
||||
{error && (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
open={error}
|
||||
autoHideDuration={2000}
|
||||
message={`Something happened during consume: ${error}`}
|
||||
onClose={handleCloseSnackbar}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumeAction;
|
||||
9
pkgs/ui/src/components/consume_content/index.tsx
Normal file
9
pkgs/ui/src/components/consume_content/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
const ConsumeDisplayComponent = ({ htmlContent }: { htmlContent: any }) => {
|
||||
return (
|
||||
<div>
|
||||
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumeDisplayComponent;
|
||||
@@ -1,26 +1,50 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Snackbar } from "@mui/material";
|
||||
import { useState, RefObject } from "react";
|
||||
import { Tooltip, Snackbar } from "@mui/material";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
|
||||
const CopyToClipboard = ({ contentRef }: { contentRef: any }) => {
|
||||
const CopyToClipboard = ({
|
||||
contentRef,
|
||||
textToCopy,
|
||||
}: {
|
||||
contentRef?: RefObject<HTMLDivElement>;
|
||||
textToCopy?: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (contentRef.current) {
|
||||
const text = contentRef.current.textContent;
|
||||
navigator.clipboard.writeText(text);
|
||||
setOpen(true);
|
||||
// Prioritize direct text copy if 'textToCopy' is provided
|
||||
const text = textToCopy || contentRef?.current?.textContent || "";
|
||||
const copiedText = textToCopy ? JSON.stringify(text, null, 2) : text;
|
||||
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(copiedText).then(
|
||||
() => {
|
||||
setOpen(true);
|
||||
},
|
||||
(err) => {
|
||||
console.error("Could not copy text: ", err);
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleClick}>Copy</Button>
|
||||
<Tooltip placement="left" title="Copy to Clipboard">
|
||||
<ContentCopyIcon onClick={handleClick} className="cursor-pointer" />
|
||||
</Tooltip>
|
||||
<Snackbar
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
autoHideDuration={2000}
|
||||
message="Copied to clipboard"
|
||||
message="Copied to clipboard!"
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyToClipboard;
|
||||
|
||||
73
pkgs/ui/src/components/copy_to_clipboard/readme.md
Normal file
73
pkgs/ui/src/components/copy_to_clipboard/readme.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# CopyToClipboard Component
|
||||
|
||||
## Overview
|
||||
|
||||
The `CopyToClipboard` component is a versatile UI component designed to facilitate copying text to the user's clipboard. It can copy text from two sources: directly from a passed text string prop (`textToCopy`) or from the text content of a referenced div element (`contentRef`). The component includes a clickable icon and displays a confirmation snackbar notification once the copy action is successful.
|
||||
|
||||
## Props
|
||||
|
||||
The component accepts the following props:
|
||||
|
||||
1. `textToCopy` (optional): A string representing the direct text you want to copy. If provided, this text is copied to the clipboard when the icon is clicked.
|
||||
|
||||
2. `contentRef` (optional): A `RefObject<HTMLDivElement>` that references a div element. The text content of this div is copied to the clipboard if `textToCopy` is not provided.
|
||||
|
||||
## Behavior
|
||||
|
||||
- Copy Action: When the copy icon is clicked, the component:
|
||||
|
||||
- Prioritizes copying the text from the `textToCopy` prop if it's provided and not an empty string.
|
||||
- If `textToCopy` is not provided or is empty, it then attempts to copy the text content of the element referenced by contentRef.
|
||||
- Uses the Clipboard API (`navigator.clipboard.writeText`) to copy the text to the user's clipboard.
|
||||
- Displays a snackbar notification confirming the copy action if successful.
|
||||
|
||||
- Snackbar Notification: A temporary notification that:
|
||||
- Appears after the text is successfully copied.
|
||||
- Displays the message "Copied to clipboard!".
|
||||
- Disappears automatically after 2000 milliseconds and is positioned at the bottom left of the screen.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Import the `CopyToClipboard` component.
|
||||
2. Use the component in one of the following ways:
|
||||
3. Pass a `textToCopy` prop with the text you want to copy, OR
|
||||
4. Pass a `contentRef` prop pointing to a div element containing the text you want to copy.
|
||||
5. Render the `CopyToClipboard` component where you want the copy icon to appear.
|
||||
|
||||
## Example
|
||||
|
||||
Using `textToCopy` prop:
|
||||
|
||||
```javascript
|
||||
import CopyToClipboard from "./CopyToClipboard";
|
||||
|
||||
const SomeComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<CopyToClipboard textToCopy="Text to be copied" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SomeComponent;
|
||||
```
|
||||
|
||||
Using `contentRef` prop:
|
||||
|
||||
```javascript
|
||||
import React, { useRef } from "react";
|
||||
import CopyToClipboard from "./CopyToClipboard";
|
||||
|
||||
const SomeComponent = () => {
|
||||
const textRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={textRef}>Text to copy from ref</div>
|
||||
<CopyToClipboard contentRef={textRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SomeComponent;
|
||||
```
|
||||
231
pkgs/ui/src/components/entity_actions/index.tsx
Normal file
231
pkgs/ui/src/components/entity_actions/index.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { IEntityActions } from "@/types";
|
||||
import {
|
||||
Button,
|
||||
Snackbar,
|
||||
Alert,
|
||||
AlertColor,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { deleteEntity } from "@/api/entities/entities";
|
||||
import axios from "axios";
|
||||
|
||||
interface Props {
|
||||
endpointData: IEntityActions[];
|
||||
rowData?: any;
|
||||
}
|
||||
|
||||
const SNACKBAR_DEFAULT = {
|
||||
open: false,
|
||||
message: "",
|
||||
severity: "info" as AlertColor,
|
||||
};
|
||||
|
||||
const EntityActions = ({ endpointData, rowData }: Props) => {
|
||||
const [snackbar, setSnackbar] = useState<{
|
||||
open: boolean;
|
||||
message: string;
|
||||
severity: AlertColor;
|
||||
}>(SNACKBAR_DEFAULT);
|
||||
|
||||
const [registerData, setRegisterData] = useState(null);
|
||||
const [registerError, setRegisterError] = useState(null);
|
||||
const [loadingRegister, setLoadingRegister] = useState(false);
|
||||
|
||||
const [DeregisterData, setDeRegisterData] = useState(null);
|
||||
const [DeregisterError, setDeRegisterError] = useState(null);
|
||||
const [loadingDeRegister, setLoadingDeRegister] = useState(false);
|
||||
|
||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
if (registerData) console.log("Register Data in state", registerData);
|
||||
if (registerError) console.error("Register Error in state", registerError);
|
||||
|
||||
if (DeregisterData) console.log("Register Data in state", DeregisterData);
|
||||
if (DeregisterError)
|
||||
console.error("Register Error in state", DeregisterError);
|
||||
|
||||
const onDeleteEntity = async () => {
|
||||
setLoadingDelete(true);
|
||||
if (rowData)
|
||||
try {
|
||||
const response = await deleteEntity({
|
||||
entity_did: rowData?.entity_did,
|
||||
});
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: response.data.message,
|
||||
severity: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting entity: ", error);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Failed to delete entity.",
|
||||
severity: "error",
|
||||
});
|
||||
} finally {
|
||||
setLoadingDelete(false);
|
||||
closeDeleteConfirmation();
|
||||
}
|
||||
};
|
||||
|
||||
const onRegisterEntity = (endpoint: string) => {
|
||||
if (loadingRegister) return;
|
||||
|
||||
setLoadingRegister(true);
|
||||
|
||||
const axiosConfig = {
|
||||
url: endpoint,
|
||||
method: "GET",
|
||||
data: null,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
axios(axiosConfig)
|
||||
.then((response) => {
|
||||
setRegisterData(response.data);
|
||||
console.log("I got the data from register: ", response.data);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "Registered successfully!",
|
||||
severity: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error happened during register: ", error);
|
||||
setRegisterError(error);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error,
|
||||
severity: "error",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingRegister(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onDeregisterEntity = (endpoint: string) => {
|
||||
if (loadingDeRegister) return;
|
||||
|
||||
setLoadingDeRegister(true);
|
||||
|
||||
const axiosConfig = {
|
||||
url: endpoint,
|
||||
method: "GET",
|
||||
data: null,
|
||||
};
|
||||
|
||||
axios(axiosConfig)
|
||||
.then((response) => {
|
||||
setDeRegisterData(response.data);
|
||||
console.log("I got the data from deregister: ", response.data);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: "De-Registered successfully!",
|
||||
severity: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error happened during deregister: ", error);
|
||||
setDeRegisterError(error);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: error,
|
||||
severity: "error",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingDeRegister(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbar(SNACKBAR_DEFAULT);
|
||||
};
|
||||
|
||||
const openDeleteConfirmation = () => {
|
||||
setConfirmDelete(true);
|
||||
};
|
||||
|
||||
const closeDeleteConfirmation = () => {
|
||||
setConfirmDelete(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
{endpointData.map(
|
||||
({ name, endpoint }: IEntityActions, index: number) => {
|
||||
const isRegister = name && name.toLocaleLowerCase() === "register";
|
||||
// const isDeRegister = name && name.toLocaleLowerCase() === "deregister";
|
||||
return (
|
||||
<Button
|
||||
disabled={loadingRegister || loadingDeRegister}
|
||||
key={index}
|
||||
onClick={() =>
|
||||
isRegister
|
||||
? onRegisterEntity(endpoint)
|
||||
: onDeregisterEntity(endpoint)
|
||||
}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<Button
|
||||
disabled={loadingDelete}
|
||||
onClick={openDeleteConfirmation}
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog open={confirmDelete} onClose={closeDeleteConfirmation}>
|
||||
<DialogTitle>Delete Entity Confirmation</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Are you sure you want to delete this entity?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={closeDeleteConfirmation}>Cancel</Button>
|
||||
<Button variant="contained" onClick={onDeleteEntity}>
|
||||
{loadingDelete ? <CircularProgress size={24} /> : `Confirm`}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
||||
open={snackbar.open}
|
||||
autoHideDuration={5000}
|
||||
onClose={handleCloseSnackbar}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleCloseSnackbar}
|
||||
severity={snackbar?.severity}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
{snackbar.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntityActions;
|
||||
49
pkgs/ui/src/components/error_boundary.tsx
Normal file
49
pkgs/ui/src/components/error_boundary.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Define a state variable to track whether is an error or not
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
console.error(error);
|
||||
// Update state so the next render will show the fallback UI
|
||||
return { hasError: true };
|
||||
}
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
// You can use your own error logging service here
|
||||
console.log({ error, errorInfo });
|
||||
}
|
||||
render(): React.ReactNode {
|
||||
// Check if the error is thrown
|
||||
if (this.state.hasError) {
|
||||
// You can render any custom fallback UI
|
||||
return (
|
||||
<div>
|
||||
<h2>Oops, there is an error!</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => this.setState({ hasError: false })}
|
||||
>
|
||||
Try again?
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Return children components in case of no error
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
49
pkgs/ui/src/components/hooks/useAxios.tsx
Normal file
49
pkgs/ui/src/components/hooks/useAxios.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { projectConfig } from "@/config/config";
|
||||
|
||||
const useAxios = (
|
||||
url: string,
|
||||
method = "GET",
|
||||
payload = null,
|
||||
isFullUrl = false,
|
||||
shouldFetch = false,
|
||||
) => {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetch = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const finalUrl = isFullUrl ? url : projectConfig.BASE_URL + url;
|
||||
|
||||
const axiosConfig = {
|
||||
url: finalUrl,
|
||||
method,
|
||||
data: payload,
|
||||
};
|
||||
|
||||
axios(axiosConfig)
|
||||
.then((response) => {
|
||||
setData(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldFetch) {
|
||||
fetch();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url, method, JSON.stringify(payload), shouldFetch]);
|
||||
|
||||
return { data, loading, error, refetch: fetch };
|
||||
};
|
||||
|
||||
export default useAxios;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { BASE_URL } from "@/constants";
|
||||
|
||||
const useFetch = (url: string) => {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetch = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get(BASE_URL + url)
|
||||
.then((response) => {
|
||||
setData(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url]);
|
||||
|
||||
return { data, loading, error, fetch };
|
||||
};
|
||||
|
||||
export default useFetch;
|
||||
93
pkgs/ui/src/components/sequence_diagram/helpers.ts
Normal file
93
pkgs/ui/src/components/sequence_diagram/helpers.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { getGroupColor, sanitizeDID } from "@/utils/helpers";
|
||||
|
||||
export const generateMermaidString = (data: any) => {
|
||||
if (!data || !data.length) return "";
|
||||
|
||||
let mermaidString = "sequenceDiagram\n";
|
||||
const participantDetails = new Map();
|
||||
|
||||
// Collect all unique participants along with their sanitized DIDs
|
||||
data.forEach((item: any) => {
|
||||
Object.values(item.groups).forEach((group: any) => {
|
||||
group.forEach((msg: any) => {
|
||||
// Apply sanitization to src_name and des_name if they are in DID format
|
||||
const sanitizedSrcName = msg.src_name.includes(":")
|
||||
? sanitizeDID(msg.src_name)
|
||||
: msg.src_name;
|
||||
const sanitizedDesName = msg.des_name.includes(":")
|
||||
? sanitizeDID(msg.des_name)
|
||||
: msg.des_name;
|
||||
|
||||
participantDetails.set(sanitizedSrcName, sanitizeDID(msg.src_did));
|
||||
participantDetails.set(sanitizedDesName, sanitizeDID(msg.des_did));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add participants to the mermaid string with names and sanitized DIDs
|
||||
participantDetails.forEach((sanitizedDID, name) => {
|
||||
mermaidString += ` participant ${name} as ${name} <br/>${sanitizedDID}\n`;
|
||||
});
|
||||
|
||||
// Iterate through each group
|
||||
data.forEach((item: any) => {
|
||||
let groupParticipants: any = new Set(); // This will collect participants for the current group
|
||||
|
||||
// Collect participants involved in each specific group
|
||||
Object.values(item.groups).forEach((group: any) => {
|
||||
group.forEach((msg: any) => {
|
||||
const sanitizedSrcName = msg.src_name.includes(":")
|
||||
? sanitizeDID(msg.src_name)
|
||||
: msg.src_name;
|
||||
const sanitizedDesName = msg.des_name.includes(":")
|
||||
? sanitizeDID(msg.des_name)
|
||||
: msg.des_name;
|
||||
|
||||
groupParticipants.add(sanitizedSrcName);
|
||||
groupParticipants.add(sanitizedDesName);
|
||||
});
|
||||
});
|
||||
|
||||
// Convert the set of participants to a sorted array and then to a string
|
||||
groupParticipants = Array.from(groupParticipants).sort().join(",");
|
||||
|
||||
// Get the group color from the config
|
||||
const groupColor = getGroupColor(item.group_name);
|
||||
|
||||
// Add group note with only involved participants
|
||||
mermaidString += `\n rect ${groupColor}\n Note over ${groupParticipants}: ${item.group_name}\n`;
|
||||
|
||||
Object.entries(item.groups).forEach(([groupId, messages]: any) => {
|
||||
mermaidString += ` alt Group Id ${groupId}\n`;
|
||||
messages.forEach((msg: any) => {
|
||||
const sanitizedSrcName = msg.src_name.includes(":")
|
||||
? sanitizeDID(msg.src_name)
|
||||
: msg.src_name;
|
||||
const sanitizedDesName = msg.des_name.includes(":")
|
||||
? sanitizeDID(msg.des_name)
|
||||
: msg.des_name;
|
||||
const arrow = sanitizedSrcName > sanitizedDesName ? "-->>" : "->>";
|
||||
mermaidString += ` ${sanitizedSrcName}${arrow}${sanitizedDesName}: [${msg.msg_type_name}]: Event Message ${msg.id}\n`;
|
||||
});
|
||||
mermaidString += " end\n";
|
||||
});
|
||||
mermaidString += " end\n";
|
||||
});
|
||||
|
||||
return mermaidString;
|
||||
};
|
||||
|
||||
export function extractAllEventMessages(data: any) {
|
||||
const allMessagesArray: any = [];
|
||||
|
||||
if (!data || data.length === 0) return allMessagesArray;
|
||||
else
|
||||
data.forEach((groupData: any) => {
|
||||
Object.values(groupData.groups).forEach((messages: any) => {
|
||||
messages.forEach((message: any) => {
|
||||
allMessagesArray.push(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
return allMessagesArray;
|
||||
}
|
||||
359
pkgs/ui/src/components/sequence_diagram/index.tsx
Normal file
359
pkgs/ui/src/components/sequence_diagram/index.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import mermaid from "mermaid";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
Tooltip,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
List,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
//Icons
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import ZoomInIcon from "@mui/icons-material/ZoomIn";
|
||||
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import ResetIcon from "@mui/icons-material/Autorenew";
|
||||
import FilterAltIcon from "@mui/icons-material/FilterAlt";
|
||||
|
||||
// Custom Components
|
||||
import { NoDataOverlay } from "../noDataOverlay";
|
||||
import { LoadingOverlay } from "../join/loadingOverlay";
|
||||
|
||||
import { useGetAllEventmessages } from "@/api/eventmessages/eventmessages";
|
||||
import { mutate } from "swr";
|
||||
|
||||
import { extractAllEventMessages, generateMermaidString } from "./helpers";
|
||||
import CopyToClipboard from "../copy_to_clipboard";
|
||||
import { formatDateTime, getGroupById } from "@/utils/helpers";
|
||||
|
||||
const SequenceDiagram = () => {
|
||||
const {
|
||||
data: eventMessagesData,
|
||||
isLoading: loadingEventMessages,
|
||||
swrKey: eventMessagesKeyFunc,
|
||||
} = useGetAllEventmessages();
|
||||
|
||||
const [scale, setScale] = useState(1);
|
||||
const [openFilters, setOpenFilters] = useState(false);
|
||||
const [sequenceNr, setSequenceNr] = useState("");
|
||||
|
||||
const mermaidRef: any = useRef(null);
|
||||
|
||||
const hasData = eventMessagesData?.data && eventMessagesData?.data.length > 0;
|
||||
const mermaidString = generateMermaidString(eventMessagesData?.data);
|
||||
const allEventMessages = extractAllEventMessages(eventMessagesData?.data);
|
||||
const dataDependency = JSON.stringify(hasData ? eventMessagesData?.data : "");
|
||||
|
||||
const userPrefersDarkmode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||
|
||||
const iconButtonColor = userPrefersDarkmode ? "default" : "primary";
|
||||
|
||||
useEffect(() => {
|
||||
const currentMermaidRef = mermaidRef?.current;
|
||||
|
||||
if (!loadingEventMessages && hasData) {
|
||||
if (
|
||||
currentMermaidRef &&
|
||||
!currentMermaidRef.getAttribute("data-processed")
|
||||
) {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "loose",
|
||||
sequence: {
|
||||
mirrorActors: true,
|
||||
showSequenceNumbers: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (currentMermaidRef) {
|
||||
currentMermaidRef.innerHTML = mermaidString;
|
||||
mermaid.init(undefined, currentMermaidRef);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (currentMermaidRef) {
|
||||
currentMermaidRef.removeAttribute("data-processed");
|
||||
currentMermaidRef.innerHTML = "";
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataDependency]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mermaidRef.current) {
|
||||
const svg = mermaidRef.current.querySelector("svg");
|
||||
if (svg) {
|
||||
svg.style.transform = `scale(${scale})`;
|
||||
svg.style.transformOrigin = "top left";
|
||||
mermaidRef.current.style.width = `${
|
||||
svg.getBoundingClientRect().width * scale
|
||||
}px`;
|
||||
mermaidRef.current.style.height = `${
|
||||
svg.getBoundingClientRect().height * scale
|
||||
}px`;
|
||||
}
|
||||
}
|
||||
}, [scale]);
|
||||
|
||||
const onRefresh = () => {
|
||||
const eventMessagesKey =
|
||||
typeof eventMessagesKeyFunc === "function"
|
||||
? eventMessagesKeyFunc()
|
||||
: eventMessagesKeyFunc;
|
||||
|
||||
if (eventMessagesKey) {
|
||||
mutate(eventMessagesKey);
|
||||
}
|
||||
};
|
||||
|
||||
const zoomIn = () => {
|
||||
setScale((scale) => scale * 1.1);
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
setScale((scale) => scale / 1.1);
|
||||
};
|
||||
|
||||
const resetZoom = () => {
|
||||
setScale(1);
|
||||
};
|
||||
|
||||
const viewInFullScreen = () => {
|
||||
if (mermaidRef.current) {
|
||||
const svg = mermaidRef.current.querySelector("svg");
|
||||
const serializer = new XMLSerializer();
|
||||
const svgBlob = new Blob([serializer.serializeToString(svg)], {
|
||||
type: "image/svg+xml",
|
||||
});
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const downloadAsPng = () => {
|
||||
if (mermaidRef.current) {
|
||||
const svg = mermaidRef.current.querySelector("svg");
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
|
||||
// Create a canvas element to convert SVG to PNG
|
||||
const canvas = document.createElement("canvas");
|
||||
const svgSize = svg.getBoundingClientRect();
|
||||
canvas.width = svgSize.width;
|
||||
canvas.height = svgSize.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = document.createElement("img");
|
||||
|
||||
img.onload = () => {
|
||||
ctx?.drawImage(img, 0, 0);
|
||||
const pngData = canvas.toDataURL("image/png");
|
||||
|
||||
// Trigger download
|
||||
const link = document.createElement("a");
|
||||
link.download = "sequence-diagram.png";
|
||||
link.href = pngData;
|
||||
link.click();
|
||||
};
|
||||
|
||||
img.src =
|
||||
"data:image/svg+xml;base64," +
|
||||
btoa(unescape(encodeURIComponent(svgData)));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFilters = () => {
|
||||
setOpenFilters((prevState) => !prevState);
|
||||
};
|
||||
|
||||
const onSearchBySeqNumber = (e: any) => {
|
||||
setSequenceNr(e.target.value);
|
||||
};
|
||||
|
||||
const isFilterMatch = (index: number) => {
|
||||
if (!sequenceNr) return true;
|
||||
|
||||
const filterSeqNrInt = parseInt(sequenceNr, 10);
|
||||
return index + 1 === filterSeqNrInt;
|
||||
};
|
||||
|
||||
if (loadingEventMessages)
|
||||
return <LoadingOverlay title="Loading Diagram" subtitle="Please wait..." />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-end">
|
||||
{hasData ? (
|
||||
<>
|
||||
<div className="flex justify-end">
|
||||
<Tooltip placement="top" title="Filter Messages">
|
||||
<IconButton color={iconButtonColor} onClick={toggleFilters}>
|
||||
<FilterAltIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title="Refresh Diagram">
|
||||
<IconButton color={iconButtonColor} onClick={onRefresh}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Zoom In" placement="top">
|
||||
<IconButton color={iconButtonColor} onClick={zoomIn}>
|
||||
<ZoomInIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Zoom Out" placement="top">
|
||||
<IconButton color={iconButtonColor} onClick={zoomOut}>
|
||||
<ZoomOutIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reset" placement="top">
|
||||
<IconButton color={iconButtonColor} onClick={resetZoom}>
|
||||
<ResetIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="View in Fullscreen" placement="top">
|
||||
<IconButton color={iconButtonColor} onClick={viewInFullScreen}>
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Download as PNG" placement="top">
|
||||
<IconButton color={iconButtonColor} onClick={downloadAsPng}>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="w-full p-2.5">
|
||||
<div className="mermaid" ref={mermaidRef}></div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex w-full justify-center">
|
||||
<NoDataOverlay label="No Activity yet" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{openFilters && (
|
||||
<>
|
||||
<Dialog
|
||||
open={openFilters}
|
||||
keepMounted
|
||||
fullWidth
|
||||
maxWidth="lg"
|
||||
onClose={toggleFilters}
|
||||
>
|
||||
<DialogTitle>All Event Messages</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<label>Search by Sequence # </label>
|
||||
<TextField
|
||||
onChange={onSearchBySeqNumber}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
<List className="w-full" component="nav">
|
||||
{allEventMessages
|
||||
.filter((_: any, index: number) => {
|
||||
return isFilterMatch(index);
|
||||
})
|
||||
.map((message: any, index: number) => {
|
||||
const {
|
||||
msg_type_name: msgType,
|
||||
des_name,
|
||||
src_name,
|
||||
group,
|
||||
group_id,
|
||||
timestamp,
|
||||
src_did,
|
||||
des_did,
|
||||
// msg, TODO: Need to use the content inside the msg to display in the diagram
|
||||
} = message;
|
||||
|
||||
const formattedTimeStamp = formatDateTime(timestamp);
|
||||
const { groupIcon: IconComponent, groupName } =
|
||||
getGroupById(group);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{ marginBottom: 12 }}
|
||||
className="flex items-center gap-5"
|
||||
>
|
||||
<Chip label={sequenceNr ? sequenceNr : ++index} />
|
||||
<Card style={{ padding: 10 }} className="w-full">
|
||||
<div
|
||||
style={{ marginBottom: 12 }}
|
||||
className="flex justify-between"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{IconComponent} {groupName}{" "}
|
||||
<Chip label={msgType} />
|
||||
</span>
|
||||
<span>
|
||||
Sender: {src_name} <Chip label={src_did} /> |{" "}
|
||||
</span>
|
||||
<span>
|
||||
Receiver: {des_name} <Chip label={des_did} />{" "}
|
||||
|{" "}
|
||||
</span>
|
||||
<span>Group: {group} | </span>
|
||||
<span>Group ID: {group_id}</span>
|
||||
</div>
|
||||
<span>{formattedTimeStamp}</span>
|
||||
</div>
|
||||
<span className="font-bold">
|
||||
Event Message {sequenceNr ? sequenceNr : index++}
|
||||
</span>
|
||||
<div
|
||||
className="mt-4 flex"
|
||||
style={{
|
||||
border: "1px solid #f1f1f1",
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<pre className="flex-1 p-2">
|
||||
{JSON.stringify(message, null, 2)}
|
||||
</pre>
|
||||
<div className="shrink-0 p-2">
|
||||
<CopyToClipboard textToCopy={message} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions className="p-4">
|
||||
<Button variant="contained" onClick={toggleFilters}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SequenceDiagram;
|
||||
@@ -6,8 +6,11 @@ import {
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import { useGetEntityByRole } from "@/api/entities/entities";
|
||||
import { Role } from "@/api/model/role";
|
||||
import Image from "next/image";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
@@ -32,22 +35,7 @@ type MenuEntry = {
|
||||
subMenuEntries?: MenuEntry[];
|
||||
};
|
||||
|
||||
const menuEntityEntries: MenuEntry[] = [
|
||||
{
|
||||
icon: <PersonIcon />,
|
||||
label: "C1",
|
||||
to: "/client/C1",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: <PersonIcon />,
|
||||
label: "C2",
|
||||
to: "/client/C2",
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const menuEntries: MenuEntry[] = [
|
||||
export const menuEntries: MenuEntry[] = [
|
||||
{
|
||||
icon: <HomeIcon />,
|
||||
label: "Home",
|
||||
@@ -83,6 +71,9 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
export function Sidebar(props: SidebarProps) {
|
||||
const { data: entityData } = useGetEntityByRole({
|
||||
role: Role.service_prosumer,
|
||||
});
|
||||
const { show, onClose } = props;
|
||||
const [activeMenuItem, setActiveMenuItem] = React.useState(
|
||||
typeof window !== "undefined" ? window.location.pathname : "",
|
||||
@@ -99,13 +90,28 @@ export function Sidebar(props: SidebarProps) {
|
||||
setCollapseMenuOpen(!collapseMenuOpen);
|
||||
};
|
||||
|
||||
const menuEntityEntries: MenuEntry[] = React.useMemo(() => {
|
||||
if (entityData) {
|
||||
return Array.isArray(entityData.data)
|
||||
? entityData.data.map((entity: any) => ({
|
||||
icon: <PersonIcon />,
|
||||
label: entity.name,
|
||||
to: entity.name,
|
||||
disabled: false,
|
||||
}))
|
||||
: [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [entityData]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isSmallerScreen) {
|
||||
setCollapseMenuOpen(false);
|
||||
} else {
|
||||
setCollapseMenuOpen(true);
|
||||
}
|
||||
}, [isSmallerScreen]);
|
||||
}, [isSmallerScreen, entityData]);
|
||||
|
||||
return (
|
||||
<aside
|
||||
@@ -125,9 +131,14 @@ export function Sidebar(props: SidebarProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:absolute lg:right-0 lg:top-0">
|
||||
<IconButton size="large" className="text-white" onClick={onClose}>
|
||||
<ChevronLeftIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={collapseMenuOpen ? "Close Sidebar" : "Expand Sidebar"}
|
||||
>
|
||||
<IconButton size="large" className="text-white" onClick={onClose}>
|
||||
<ChevronLeftIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Divider
|
||||
@@ -195,31 +206,36 @@ export function Sidebar(props: SidebarProps) {
|
||||
unmountOnExit
|
||||
>
|
||||
<List component="div" disablePadding>
|
||||
{menuEntityEntries.map((menuEntry, idx) => (
|
||||
<ListItemButton
|
||||
key={idx}
|
||||
sx={{ pl: 4 }}
|
||||
className="lg:justify-normal"
|
||||
LinkComponent={Link}
|
||||
href={menuEntry.to}
|
||||
disabled={menuEntry.disabled}
|
||||
selected={activeMenuItem === menuEntry.to}
|
||||
onClick={() => handleMenuItemClick(menuEntry.to)}
|
||||
{menuEntityEntries?.map((menuEntry, idx) => (
|
||||
<Link
|
||||
key={"entity-link-" + idx}
|
||||
href={`/client?name=${menuEntry.to}`}
|
||||
style={{ textDecoration: "none", color: "white" }}
|
||||
>
|
||||
<ListItemIcon
|
||||
color="inherit"
|
||||
className="overflow-hidden text-white lg:justify-normal"
|
||||
<ListItemButton
|
||||
key={idx}
|
||||
sx={{ pl: 4 }}
|
||||
className="lg:justify-normal"
|
||||
LinkComponent={Link}
|
||||
disabled={menuEntry.disabled}
|
||||
selected={activeMenuItem === menuEntry.to}
|
||||
onClick={() => handleMenuItemClick(menuEntry.to)}
|
||||
>
|
||||
{menuEntry.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={menuEntry.label}
|
||||
primaryTypographyProps={{
|
||||
color: "inherit",
|
||||
}}
|
||||
className="hidden lg:block"
|
||||
/>
|
||||
</ListItemButton>
|
||||
<ListItemIcon
|
||||
color="inherit"
|
||||
className="overflow-hidden text-white lg:justify-normal"
|
||||
>
|
||||
{menuEntry.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={menuEntry.label}
|
||||
primaryTypographyProps={{
|
||||
color: "inherit",
|
||||
}}
|
||||
className="hidden lg:block"
|
||||
/>
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
|
||||
@@ -13,29 +13,17 @@ import { EntityDetails, ISummaryDetails } from "@/types";
|
||||
const SummaryDetails = ({
|
||||
entity,
|
||||
hasRefreshButton,
|
||||
hasAttachDetach,
|
||||
fake,
|
||||
onRefresh,
|
||||
}: ISummaryDetails) => {
|
||||
const cardContentRef = useRef(null);
|
||||
const cardContentRef = useRef<HTMLDivElement>(null);
|
||||
const hasDetails = entity.details && entity.details.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2>{entity.name}</h2>
|
||||
<div>
|
||||
{hasAttachDetach && (
|
||||
<Button className="mr-6" variant="contained">
|
||||
Attach / Detach
|
||||
</Button>
|
||||
)}
|
||||
{hasRefreshButton && (
|
||||
<Button onClick={onRefresh} variant="contained">
|
||||
Refresh
|
||||
@@ -46,7 +34,7 @@ const SummaryDetails = ({
|
||||
{hasDetails && (
|
||||
<Card variant="outlined">
|
||||
<CardHeader
|
||||
subheader={fake ? "Summary (Fake Data)" : "Summary"}
|
||||
subheader={`Summary ${fake ? "(Fake Data)" : ""}`}
|
||||
action={<CopyToClipboard contentRef={cardContentRef} />}
|
||||
/>
|
||||
<CardContent ref={cardContentRef}>
|
||||
|
||||
52
pkgs/ui/src/components/summary_card/readme.md
Normal file
52
pkgs/ui/src/components/summary_card/readme.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# SummaryDetails Component
|
||||
|
||||
## Overview
|
||||
|
||||
The `SummaryDetails` component is a flexible UI component designed to display a summary of details related to a specific entity in a card format. It is equipped with optional functionalities such as refreshing the data.
|
||||
|
||||
## Props
|
||||
|
||||
The component accepts the following props:
|
||||
|
||||
1. `entity`: An object representing the entity whose details are to be displayed. It should have a name and details, where details is an array of `EntityDetails` objects.
|
||||
|
||||
2. `hasRefreshButton` (optional): A boolean indicating if a Refresh button should be displayed. If true, the button is shown, allowing the user to refresh the entity details.
|
||||
|
||||
3. `fake` (optional): A boolean indicating if the displayed data is fake. If true, a label '(Fake Data)' is displayed in the card's header.
|
||||
|
||||
4. `onRefresh` (optional): A function to be called when the Refresh button is clicked. It should handle the logic for refreshing the entity details.
|
||||
|
||||
## UI Structure
|
||||
|
||||
- The component starts with a flex container displaying the entity's name and optional button (Refresh) based on the props.
|
||||
- If the entity has details (checked by `hasDetails`), it displays a card containing:
|
||||
- A `CardHeader` with a subheader indicating it's a summary and whether the data is fake.
|
||||
- A `CopyToClipboard` component attached to the card's action, allowing the user to copy the details.
|
||||
- A `CardContent` section listing all the details. Each detail is displayed as a `Typography` component, showing the label and value of each `EntityDetails` item.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Import the `SummaryDetails` component.
|
||||
2. Create an entity object with a name and details, where details is an array of objects with label and value.
|
||||
3. Optionally, decide if you want the Refresh functionality by setting `hasRefreshButton` to `true`.
|
||||
4. If using the `Refresh` functionality, provide an `onRefresh` function to handle the logic.
|
||||
5. Render the `SummaryDetails` component with the desired props.
|
||||
|
||||
## Example
|
||||
|
||||
```javascript
|
||||
<SummaryDetails
|
||||
entity={{
|
||||
name: "Sample Entity",
|
||||
details: [
|
||||
{ label: "Detail 1", value: "Value 1" },
|
||||
{ label: "Detail 2", value: "Value 2" },
|
||||
// ... more details
|
||||
],
|
||||
}}
|
||||
hasRefreshButton={true}
|
||||
onRefresh={() => {
|
||||
// handle refresh button logic/callback
|
||||
}}
|
||||
/>
|
||||
```
|
||||
@@ -10,8 +10,15 @@ import { NoDataOverlay } from "@/components/noDataOverlay";
|
||||
import { StyledTableCell, StyledTableRow } from "./style";
|
||||
import { ICustomTable, CustomTableConfiguration } from "@/types";
|
||||
import { Checkbox, Skeleton } from "@mui/material";
|
||||
import ErrorBoundary from "@/components/error_boundary";
|
||||
|
||||
const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
|
||||
const CustomTable = ({
|
||||
configuration,
|
||||
data,
|
||||
loading,
|
||||
tkey,
|
||||
onConsumeAction,
|
||||
}: ICustomTable) => {
|
||||
if (loading)
|
||||
return <Skeleton variant="rectangular" animation="wave" height={200} />;
|
||||
|
||||
@@ -22,7 +29,12 @@ const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
|
||||
const renderTableCell = (
|
||||
value: any,
|
||||
cellKey: string,
|
||||
render?: (param: any) => void | undefined,
|
||||
render?: (
|
||||
param: any,
|
||||
data?: any,
|
||||
onFunc?: (param: any) => void,
|
||||
) => void | undefined,
|
||||
rowData?: any,
|
||||
) => {
|
||||
let renderedValue = value;
|
||||
|
||||
@@ -34,18 +46,29 @@ const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
|
||||
renderedValue = <Checkbox disabled checked={value} />;
|
||||
|
||||
// cover use case if we want to render a component
|
||||
if (render) renderedValue = render(value);
|
||||
if (render) renderedValue = render(value, rowData, onConsumeAction);
|
||||
|
||||
// catch use case where the value is an object but the render function is not provided in the table config
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
render === undefined
|
||||
) {
|
||||
console.warn("Missing render function for column " + cellKey);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledTableCell key={cellKey} align="left">
|
||||
{renderedValue}
|
||||
</StyledTableCell>
|
||||
<ErrorBoundary>
|
||||
<StyledTableCell key={cellKey} align="left">
|
||||
{renderedValue}
|
||||
</StyledTableCell>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 700 }} aria-label="customized table">
|
||||
<TableContainer component={Paper} style={{ maxHeight: 350 }}>
|
||||
<Table stickyHeader sx={{ minWidth: 700 }} aria-label="customized table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{configuration.map((header: CustomTableConfiguration) => (
|
||||
@@ -54,17 +77,18 @@ const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((data: any, rowIndex: number) => (
|
||||
{data.map((rowData: any, rowIndex: number) => (
|
||||
<StyledTableRow key={rowIndex}>
|
||||
{configuration.map(
|
||||
(column: CustomTableConfiguration, columnIndex: number) => {
|
||||
const cellValue: any = data[column.key];
|
||||
const cellValue: any = rowData[column.key];
|
||||
const cellKey = tkey + ":" + column.key + ":" + rowIndex;
|
||||
const renderComponent = column?.render;
|
||||
return renderTableCell(
|
||||
cellValue,
|
||||
cellKey + ":" + columnIndex,
|
||||
renderComponent,
|
||||
rowData,
|
||||
);
|
||||
},
|
||||
)}
|
||||
|
||||
75
pkgs/ui/src/components/table/readme.md
Normal file
75
pkgs/ui/src/components/table/readme.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# CustomTable Component
|
||||
|
||||
## Overview
|
||||
|
||||
The `CustomTable` component is a dynamic and flexible table designed to display data in a structured tabular format. It is highly customizable, allowing specific rendering for different data types and providing a user-friendly display for loading and empty data states.
|
||||
|
||||
## Props
|
||||
|
||||
The component accepts the following props:
|
||||
|
||||
1. `configuration`: An array of `CustomTableConfiguration` objects defining the structure and customization options for table columns, including:
|
||||
|
||||
- `key`: Corresponds to the key in the data objects for the column.
|
||||
- `label`: Text label for the column header.
|
||||
- `render` (optional): A function for custom rendering of the cell's content.
|
||||
- `data`: An array of data objects, each representing a row in the table.
|
||||
- `loading` (optional): If `true`, displays a loading state (skeleton screen).
|
||||
- `tkey`: A unique key for the table, used for constructing unique cell keys.
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Loading State**: Displays a `Skeleton` loader when `loading` is `true`.
|
||||
- **Empty Data State**: Displays a `NoDataOverlay` component with a message if no data is available.
|
||||
- **Data Rendering**:
|
||||
- Dynamically renders cells based on `configuration`.
|
||||
- Handles different data types:
|
||||
- Joins array elements with a comma.
|
||||
- Shows a disabled checkbox for boolean values.
|
||||
- Uses the provided `render` function for custom rendering.
|
||||
- Logs a warning if a cell's value is an object (not an array), and no `render` function is provided.
|
||||
- **Error Handling**: Each cell is wrapped in an `ErrorBoundary` component for graceful error handling.
|
||||
|
||||
## How to Use
|
||||
|
||||
1. Import the `CustomTable` component.
|
||||
2. Define the `configuration` for table columns.
|
||||
3. Provide `data` as an array of objects corresponding to the configuration.
|
||||
4. Optionally, control the loading state with the `loading` prop.
|
||||
5. Provide a unique `tkey` for the table.
|
||||
|
||||
## Example
|
||||
|
||||
```javascript
|
||||
import CustomTable from "./CustomTable";
|
||||
|
||||
const tableConfig = [
|
||||
{ key: "name", label: "Name" },
|
||||
{ key: "age", label: "Age" },
|
||||
{
|
||||
key: "isActive",
|
||||
label: "Active",
|
||||
render: (isActive) => (isActive ? "Yes" : "No"),
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = [
|
||||
{ name: "John Doe", age: 30, isActive: true },
|
||||
{ name: "Jane Smith", age: 25, isActive: false },
|
||||
];
|
||||
|
||||
const SomeComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<CustomTable
|
||||
configuration={tableConfig}
|
||||
data={tableData}
|
||||
loading={false}
|
||||
tkey="unique-table-key"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SomeComponent;
|
||||
```
|
||||
@@ -4,7 +4,6 @@ import TableRow from "@mui/material/TableRow";
|
||||
|
||||
export const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
// backgroundColor: theme.palette.common.black,
|
||||
backgroundColor: "#003258",
|
||||
color: theme.palette.common.white,
|
||||
},
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
// AP - Summary
|
||||
|
||||
export const APSummaryDetails = [
|
||||
{
|
||||
label: "DID",
|
||||
value: "did:sov:test:1274",
|
||||
},
|
||||
{
|
||||
label: "IP",
|
||||
value: "127.0.0.2",
|
||||
},
|
||||
{
|
||||
label: "Network",
|
||||
value: "Carlo's Home Network",
|
||||
},
|
||||
];
|
||||
// AP - 2 Tables Configurations to display labels
|
||||
|
||||
export const APAttachmentsTableConfig = [
|
||||
{
|
||||
@@ -59,21 +44,10 @@ export const APServiceRepositoryTableConfig = [
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: "Type",
|
||||
render: (value: any) => {
|
||||
let renderedValue: any = "";
|
||||
if (typeof value === "object") {
|
||||
const label = Object.keys(value)[0];
|
||||
const info = value[label];
|
||||
renderedValue = (
|
||||
<code>
|
||||
{label} {info}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value.data)) renderedValue = value.data.join(", ");
|
||||
else console.error("Status is not an array", value);
|
||||
return renderedValue;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from "@mui/material";
|
||||
import EntityActions from "@/components/entity_actions";
|
||||
import ConsumeAction from "@/components/consume_action";
|
||||
|
||||
export const ClientTableConfig = [
|
||||
{
|
||||
@@ -12,11 +13,13 @@ export const ClientTableConfig = [
|
||||
{
|
||||
key: "endpoint_url",
|
||||
label: "End Point",
|
||||
render: () => {
|
||||
render: (value: any, rowData: any, onConsume: any) => {
|
||||
return (
|
||||
<Button disabled variant="outlined">
|
||||
Consume
|
||||
</Button>
|
||||
<ConsumeAction
|
||||
rowData={rowData}
|
||||
onConsume={onConsume}
|
||||
endpoint={value}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -52,26 +55,43 @@ export const ServiceTableConfig = [
|
||||
label: "Entity DID",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: "Action",
|
||||
key: "usage",
|
||||
label: "Usage",
|
||||
render: (value: any) => {
|
||||
let renderedValue: any = "";
|
||||
if (typeof value === "object")
|
||||
renderedValue = (
|
||||
<>
|
||||
{value.action.map((actionType: string) => (
|
||||
<>
|
||||
<code>{actionType}</code>
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
let renderedValue = "";
|
||||
|
||||
if (value.length > 0) {
|
||||
renderedValue = value.map((item: any, index: number) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{item.consumer_entity_did} ({item.times_consumed})
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
return renderedValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
render: (value: any) => {
|
||||
let renderedValue: any = "";
|
||||
if (Array.isArray(value.data)) {
|
||||
renderedValue = value.data.join(", ");
|
||||
} else {
|
||||
console.error("Status is not an array", value);
|
||||
}
|
||||
return renderedValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
label: "Actions",
|
||||
render: (value: any, rowData?: any) => {
|
||||
if (value && value?.data.length > 0)
|
||||
return <EntityActions rowData={rowData} endpointData={value.data} />;
|
||||
else return "N/A";
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
export const Client2ConsumerTableConfig = [
|
||||
{
|
||||
key: "service_name",
|
||||
label: "Service name",
|
||||
},
|
||||
{
|
||||
key: "service_type",
|
||||
label: "Service Type",
|
||||
},
|
||||
{
|
||||
key: "endpoint_url",
|
||||
label: "End Point",
|
||||
render: () => {
|
||||
return (
|
||||
<Button disabled variant="outlined">
|
||||
Consume
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: "entity",
|
||||
// label: "Entity",
|
||||
// },
|
||||
{
|
||||
key: "entity_did",
|
||||
label: "Entity DID",
|
||||
},
|
||||
// {
|
||||
// key: "network",
|
||||
// label: "Network",
|
||||
// },
|
||||
];
|
||||
|
||||
export const Client2ProducerTableConfig = [
|
||||
{
|
||||
key: "service_name",
|
||||
label: "Service name",
|
||||
},
|
||||
{
|
||||
key: "service_type",
|
||||
label: "Service Type",
|
||||
},
|
||||
{
|
||||
key: "endpoint_url",
|
||||
label: "End Point",
|
||||
},
|
||||
{
|
||||
key: "entity_did",
|
||||
label: "Entity DID",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: "Action",
|
||||
render: (value: any) => {
|
||||
let renderedValue: any = "";
|
||||
if (typeof value === "object")
|
||||
renderedValue = (
|
||||
<>
|
||||
{value.action.map((actionType: string) => (
|
||||
<>
|
||||
<code>{actionType}</code>
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
return renderedValue;
|
||||
},
|
||||
},
|
||||
];
|
||||
112
pkgs/ui/src/config/config.tsx
Normal file
112
pkgs/ui/src/config/config.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import AttachmentIcon from "@mui/icons-material/Attachment";
|
||||
import ArticleIcon from "@mui/icons-material/Article";
|
||||
import ConstructionIcon from "@mui/icons-material/Construction";
|
||||
import AssignmentTurnedInIcon from "@mui/icons-material/AssignmentTurnedIn";
|
||||
import RemoveCircleIcon from "@mui/icons-material/RemoveCircle";
|
||||
import AddCircleIcon from "@mui/icons-material/AddCircle";
|
||||
import PageviewIcon from "@mui/icons-material/Pageview";
|
||||
import BuildIcon from "@mui/icons-material/Build";
|
||||
|
||||
export const projectConfig: any = {
|
||||
BASE_URL: "http://localhost:2979/api/v1",
|
||||
REFRESH_FREQUENCY: 5000,
|
||||
GROUPS: [
|
||||
{
|
||||
groupName: "Attachement",
|
||||
groupId: 1,
|
||||
groupColor: "rgb(230, 230, 250)",
|
||||
groupIcon: <AttachmentIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "Attachment Request Send" },
|
||||
{ id: 2, label: "Attachment Request Received" },
|
||||
{ id: 3, label: "Attachment Response Send" },
|
||||
{ id: 4, label: "Attachment Response Received" },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Connection Setup",
|
||||
groupId: 2,
|
||||
groupColor: "rgb(245, 222, 179)",
|
||||
groupIcon: <ConstructionIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "Connection request send" },
|
||||
{ id: 2, label: "Connection request received" },
|
||||
{ id: 3, label: "Connection response send" },
|
||||
{ id: 4, label: "Connection response received" },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Presentation",
|
||||
groupId: 3,
|
||||
groupColor: "rgb(255, 209, 220)",
|
||||
groupIcon: <ArticleIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "Request send" },
|
||||
{ id: 2, label: "Request received" },
|
||||
{ id: 3, label: "Presentation send" },
|
||||
{ id: 4, label: "Presentation received" },
|
||||
{ id: 5, label: "Presentation acknowledged" },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "DID Resolution",
|
||||
groupId: 4,
|
||||
groupColor: "rgb(189, 255, 243)",
|
||||
groupIcon: <AssignmentTurnedInIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "DID Resolution Request send" },
|
||||
{ id: 2, label: "DID Resolution Request received" },
|
||||
{ id: 3, label: "DID Resolution Response send" },
|
||||
{ id: 4, label: "DID Resolution Response received" },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Service De-registration",
|
||||
groupId: 5,
|
||||
groupColor: "rgb(255, 218, 185)",
|
||||
groupIcon: <RemoveCircleIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "Service De-registration send" },
|
||||
{ id: 2, label: "Service De-registration received" },
|
||||
{ id: 3, label: "Service De-registration successful send" },
|
||||
{ id: 4, label: "Service De-registration successful received" },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Service Registration",
|
||||
groupId: 6,
|
||||
groupColor: "rgb(200, 162, 200)",
|
||||
groupIcon: <AddCircleIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "Service Registration send" },
|
||||
{ id: 2, label: "Service Registration received" },
|
||||
{ id: 3, label: "Service Registration successful send" },
|
||||
{ id: 4, label: "Service Registration successful received" },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Service Discovery",
|
||||
groupId: 7,
|
||||
groupColor: "rgb(255, 250, 205)",
|
||||
groupIcon: <PageviewIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "Service Discovery send" },
|
||||
{ id: 2, label: "Service Discovery received" },
|
||||
{ id: 3, label: "Service Discovery Result send" },
|
||||
{ id: 4, label: "Service Discovery Result received" },
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Service Operation",
|
||||
groupId: 8,
|
||||
groupColor: "rgb(135, 206, 235)",
|
||||
groupIcon: <BuildIcon />,
|
||||
messageTypes: [
|
||||
{ id: 1, label: "Service Request Send" },
|
||||
{ id: 2, label: "Service Request Received" },
|
||||
{ id: 3, label: "Service Response Send" },
|
||||
{ id: 4, label: "Service Response Received" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,19 +1,6 @@
|
||||
// DLG Summary Details
|
||||
|
||||
import { formatDateTime } from "@/utils/helpers";
|
||||
|
||||
export const DLGSummaryDetails = [
|
||||
{
|
||||
label: "DID",
|
||||
value: "did:sov:test:1274",
|
||||
},
|
||||
{
|
||||
label: "URL",
|
||||
value: "dlg.tu-berlin.de",
|
||||
},
|
||||
];
|
||||
|
||||
// DLG Resolution Table
|
||||
// DLG - 2 Tables Configurations to display labels
|
||||
|
||||
export const DLGResolutionDummyData = [
|
||||
{
|
||||
|
||||
@@ -10,10 +10,6 @@ export const HomeTableConfig = [
|
||||
{
|
||||
key: "network",
|
||||
label: "Network",
|
||||
render: (value: any) => {
|
||||
const renderedValue = typeof value === "object" ? value?.network : "-";
|
||||
return renderedValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "ip",
|
||||
@@ -22,11 +18,6 @@ export const HomeTableConfig = [
|
||||
{
|
||||
key: "roles",
|
||||
label: "Roles",
|
||||
render: (value: any) => {
|
||||
const renderedValue =
|
||||
typeof value === "object" ? value?.roles?.join(", ") : "-";
|
||||
return renderedValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "attached",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
const BASE_URL = "http://localhost:2979/api/v1";
|
||||
|
||||
// Home View
|
||||
const HOME_VIEW_TABLE = "/get_entities";
|
||||
|
||||
// Access Point
|
||||
const SERVICE_REPOSITORY_URL = "/get_repositories";
|
||||
|
||||
export { BASE_URL, HOME_VIEW_TABLE, SERVICE_REPOSITORY_URL };
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface CustomTableConfiguration {
|
||||
key: string;
|
||||
label: string;
|
||||
render?: (param: any) => void;
|
||||
render?: (param: any, rowData?: any, onConsume?: any) => void;
|
||||
}
|
||||
|
||||
export interface ICustomTable {
|
||||
@@ -9,22 +9,27 @@ export interface ICustomTable {
|
||||
data: any;
|
||||
loading?: boolean;
|
||||
tkey: string;
|
||||
onConsumeAction?: (param: any) => void;
|
||||
}
|
||||
|
||||
export interface EntityDetails {
|
||||
label: string;
|
||||
value: string;
|
||||
value: string | undefined;
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
name: string;
|
||||
name?: string;
|
||||
details: EntityDetails[];
|
||||
}
|
||||
|
||||
export interface ISummaryDetails {
|
||||
entity: any;
|
||||
entity: Entity;
|
||||
fake?: boolean;
|
||||
hasRefreshButton?: boolean;
|
||||
hasAttachDetach?: boolean;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
export interface IEntityActions {
|
||||
name: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const formatDateTime = (date: string) => {
|
||||
const _date = new Date(date);
|
||||
import { projectConfig } from "@/config/config";
|
||||
|
||||
export const formatDateTime = (date: string | number) => {
|
||||
const dateToFormat = typeof date === "number" ? date * 1000 : date;
|
||||
const _date = new Date(dateToFormat);
|
||||
return _date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
@@ -10,3 +13,19 @@ export const formatDateTime = (date: string) => {
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
|
||||
export function sanitizeDID(did: string) {
|
||||
return did.replace(/:/g, "_");
|
||||
}
|
||||
|
||||
export function getGroupColor(groupName: string) {
|
||||
const group = projectConfig.GROUPS.find(
|
||||
(g: any) => g.groupName === groupName,
|
||||
);
|
||||
return group ? group.groupColor : "rgb(211, 211, 211)"; // Light gray if not found
|
||||
}
|
||||
|
||||
export function getGroupById(groupId: string | number) {
|
||||
const group = projectConfig.GROUPS.find((g: any) => g.groupId === groupId);
|
||||
return group ? group : {};
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ module.exports = {
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
safelist: ["mermaid"],
|
||||
important: "#__next",
|
||||
theme: {
|
||||
colors: {
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
"@API/*": ["./src/api/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"build/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user