Compare commits

...

71 Commits

Author SHA1 Message Date
Arslan, Erdem
102e38988c enable debug mode for release pipeline 2024-01-30 12:48:57 +01:00
Arslan, Erdem
4e7c44eeb5 update tags for release pipeline 2024-01-30 12:42:27 +01:00
Arslan, Erdem
3238cdf36d update release pipeline 2024-01-30 12:23:14 +01:00
Arslan, Erdem
1abbc383ec create .gitlab-ci.yml for a release pipeline 2024-01-30 12:22:54 +01:00
5a09cc8e31 update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-29 20:02:50 +00:00
Sara Pervana
c6e1b8a21d Merge pull request 'added dark mode switch for icon buttons' (#77) from dark-mode-buttons into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m13s
assets1 / test (push) Has been cancelled
Reviewed-on: #77
2024-01-29 21:00:33 +01:00
sara-pervana
afe291c54c added dark mode switch for icon buttons
All checks were successful
checks-impure / test (pull_request) Successful in 28s
checks / test (pull_request) Successful in 3m47s
2024-01-29 20:53:16 +01:00
6024090f69 update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-29 18:40:49 +00:00
Sara Pervana
596e87c31b Merge pull request 'Adding Consume Functionality' (#74) from consume-functionality into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m10s
assets1 / test (push) Has been cancelled
Reviewed-on: #74
2024-01-29 19:38:27 +01:00
sara-pervana
06c55f151b fixed invalid tailwind classes 2024-01-29 19:38:27 +01:00
sara-pervana
04675f5e9e added a bit of time delay for register/deregister and also a confirm button for delete 2024-01-29 19:38:27 +01:00
sara-pervana
b5008306cb added consume view and loaders on the buttons when clicked 2024-01-29 19:38:27 +01:00
sara-pervana
1b549549c0 added headers for axios requests 2024-01-29 19:38:27 +01:00
sara-pervana
29aa17ca7c added consume and register deregister as simple fetch 2024-01-29 19:38:27 +01:00
sara-pervana
e06afab048 minimum progress 2024-01-29 19:38:27 +01:00
sara-pervana
697d2685f3 small formatting fixes 2024-01-29 19:38:27 +01:00
Georg-Stahn
2f6ad476b3 Merge pull request 'added middleware for emulation' (#76) from georgdeamon into main
All checks were successful
checks-impure / test (push) Successful in 25s
checks / test (push) Successful in 1m24s
assets1 / test (push) Successful in 23s
Reviewed-on: #76
2024-01-28 19:37:23 +01:00
Georg-Stahn
dc04001dca emulate nix fmt changes
All checks were successful
checks-impure / test (pull_request) Successful in 26s
checks / test (pull_request) Successful in 3m35s
2024-01-28 18:26:00 +01:00
Georg-Stahn
3828402865 added middleware for emulation
Some checks failed
checks-impure / test (pull_request) Successful in 26s
checks / test (pull_request) Failing after 3m38s
2024-01-28 18:22:32 +01:00
Georg-Stahn
5b64eb4c3f Merge pull request 'georgdeamon' (#75) from georgdeamon into main
All checks were successful
checks-impure / test (push) Successful in 25s
checks / test (push) Successful in 1m12s
assets1 / test (push) Successful in 21s
Reviewed-on: #75
2024-01-26 11:47:50 +01:00
Georg-Stahn
eebc7eee20 filter out ap dlg in entity view
All checks were successful
checks-impure / test (pull_request) Successful in 26s
checks / test (pull_request) Successful in 3m19s
2024-01-26 09:18:04 +01:00
Georg-Stahn
2e787aa386 change deamon0 8000 to deamon1 8001 as emulation 2024-01-26 08:43:43 +01:00
Georg-Stahn
90c6df93f4 change deamon0 8000 to deamon1 8001 as emulation 2024-01-26 08:38:45 +01:00
374fdfdaea update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-26 00:28:17 +00:00
Sara Pervana
a240cfd340 Merge pull request 'Added a lot of fixes' (#73) from more-fixes into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m14s
assets1 / test (push) Has been cancelled
Reviewed-on: #73
2024-01-26 01:25:58 +01:00
sara-pervana
eaec0feb96 warning fix
All checks were successful
checks-impure / test (pull_request) Successful in 26s
checks / test (pull_request) Successful in 3m3s
2024-01-26 01:19:32 +01:00
sara-pervana
f3ab6e1b45 removed warnings
Some checks failed
checks-impure / test (pull_request) Successful in 26s
checks / test (pull_request) Failing after 1m48s
2024-01-26 01:11:55 +01:00
sara-pervana
2c79dabce0 added a lot of stuff
Some checks failed
checks-impure / test (pull_request) Successful in 33s
checks / test (pull_request) Failing after 2m47s
2024-01-26 00:44:53 +01:00
sara-pervana
fd09d73edd small formatting fixes 2024-01-25 22:31:25 +01:00
sara-pervana
2000c1444a fix the issue of mermaid chart not rendering when changing routes 2024-01-25 21:56:18 +01:00
sara-pervana
b98c6090af remove fake data in the header of dlg and ap views 2024-01-25 20:51:45 +01:00
42a25a2eda update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-25 14:08:59 +00:00
b6b2bfbee5 Merge pull request 'Fixed sidebar and did problem' (#72) from Qubasa-main into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m14s
assets1 / test (push) Has been cancelled
Reviewed-on: #72
2024-01-25 15:06:37 +01:00
09f80b1f42 Fixed sidebar and did problem
All checks were successful
checks-impure / test (pull_request) Successful in 26s
checks / test (pull_request) Successful in 1m13s
2024-01-25 14:47:52 +01:00
170ada9382 Fixed sidebar and did problem 2024-01-25 14:47:37 +01:00
a8b472e84d update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-25 13:39:23 +00:00
Erdem-Arslan
cf3fc347de Merge pull request 'dynamic-routing-sidebar' (#71) from dynamic-routing-sidebar into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m12s
assets1 / test (push) Has been cancelled
Reviewed-on: #71
2024-01-25 14:37:03 +01:00
Arslan, Erdem
1d39ebcddc fix formatting
All checks were successful
checks-impure / test (pull_request) Successful in 29s
checks / test (pull_request) Successful in 3m47s
2024-01-25 14:32:11 +01:00
Arslan, Erdem
d978e413d5 filter out AP and DLG entities 2024-01-25 14:08:52 +01:00
8d72268922 update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-25 01:03:29 +00:00
Sara Pervana
840b3b6972 Merge pull request 'A lot of Diagram Fixes as well as other code changes' (#70) from diagram-fixes into main
Some checks failed
checks-impure / test (push) Successful in 25s
checks / test (push) Successful in 1m11s
assets1 / test (push) Has been cancelled
Reviewed-on: #70
2024-01-25 02:01:10 +01:00
sara-pervana
db1591a76e final final tailwind fixes 2024-01-25 02:01:10 +01:00
sara-pervana
1d6ea0ef0c afain fixed tailwind classes 2024-01-25 02:01:10 +01:00
sara-pervana
711ade4866 fixing some failuers on build 2024-01-25 02:01:10 +01:00
sara-pervana
7779441c87 final fixes for the diagram 2024-01-25 02:01:10 +01:00
sara-pervana
8e92415c34 small change to readme 2024-01-25 02:01:10 +01:00
sara-pervana
68e2f6d683 removed warnings 2024-01-25 02:01:10 +01:00
sara-pervana
c03da10e98 current progress with diagram and project fixes 2024-01-25 02:01:10 +01:00
60205b3c22 Merge pull request 'fix src_name and des_name being NULL' (#69) from Qubasa-main into main
All checks were successful
checks-impure / test (push) Successful in 28s
checks / test (push) Successful in 3m50s
assets1 / test (push) Successful in 22s
Reviewed-on: #69
2024-01-24 23:50:25 +01:00
da99e71b54 fix src_name and des_name being NULL 2024-01-24 23:50:25 +01:00
b978aabdd6 update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-24 22:42:32 +00:00
357568ebcb Merge pull request 'Fixed upload script' (#68) from Qubasa-main into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m18s
assets1 / test (push) Has been cancelled
Reviewed-on: #68
2024-01-24 23:40:08 +01:00
ea2be8b7c7 nix fmt
All checks were successful
checks-impure / test (pull_request) Successful in 29s
checks / test (pull_request) Successful in 3m53s
2024-01-24 23:33:28 +01:00
672c8364bc Fixed upload script 2024-01-24 23:33:02 +01:00
2b8a9d9316 Merge pull request 'Fixed wrong ordering of eventmessages' (#66) from Qubasa-main into main
All checks were successful
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m12s
assets1 / test (push) Successful in 22s
Reviewed-on: #66
2024-01-24 22:47:06 +01:00
9b6fc699f2 Added push_docker script
All checks were successful
checks-impure / test (pull_request) Successful in 28s
checks / test (pull_request) Successful in 3m47s
2024-01-24 22:41:29 +01:00
d232510c0e Added build_docker.sh script 2024-01-24 22:41:29 +01:00
dcaecba393 Fixed incorrect grouping, in eventmessages 2024-01-24 22:41:03 +01:00
7901712c4c update ui-assets.nix
All checks were successful
checks-impure / test (push) Has been skipped
checks / test (push) Has been skipped
assets1 / test (push) Has been skipped
2024-01-24 21:26:10 +00:00
Erdem-Arslan
48df7352da Merge pull request 'dynamic-routing-sidebar' (#67) from dynamic-routing-sidebar into main
Some checks failed
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m14s
assets1 / test (push) Has been cancelled
Reviewed-on: #67
2024-01-24 22:23:50 +01:00
Arslan, Erdem
c5c4ab7178 remove variable index within mapping
All checks were successful
checks-impure / test (pull_request) Successful in 26s
checks / test (pull_request) Successful in 3m10s
2024-01-24 22:19:46 +01:00
Arslan, Erdem
22bcbf6819 fix formatting
Some checks failed
checks-impure / test (pull_request) Successful in 27s
checks / test (pull_request) Failing after 1m51s
2024-01-24 22:14:19 +01:00
Arslan, Erdem
2ab2282116 implement dynamic routing within the sidebar 2024-01-24 22:12:00 +01:00
1c6e33e74f Merge pull request 'Fixed wrong ordering of eventmessages' (#65) from Qubasa-main into main
All checks were successful
checks-impure / test (push) Successful in 26s
checks / test (push) Successful in 1m13s
assets1 / test (push) Successful in 22s
Reviewed-on: #65
2024-01-24 18:51:56 +01:00
1757bf1952 Fixed wrong ordering of eventmessages 2024-01-24 18:51:56 +01:00
Arslan, Erdem
ea0148fdaf formatting index.tsx 2024-01-21 21:56:03 +01:00
Arslan, Erdem
7ae1d5f768 create loading spinner for the consumer button 2024-01-21 21:34:53 +01:00
Arslan, Erdem
ec67dd1bac updating fetch request 2024-01-21 21:24:17 +01:00
Arslan, Erdem
522d7eb69a fetch the consume and register/deregister endpoints and add error handling 2024-01-21 21:15:51 +01:00
Arslan, Erdem
07a5a2fc24 Merge branch 'main' into register-deregister-actions
# Conflicts:
#	pkgs/ui/src/components/sequence_diagram/index.tsx
2024-01-21 20:45:24 +01:00
sara-pervana
59e33f3ead add register, deregister actions 2024-01-21 16:49:34 +01:00
42 changed files with 1558 additions and 592 deletions

2
.gitignore vendored
View File

@@ -23,3 +23,5 @@ htmlcov
# georgs
pkgs/.vs/
pkgs/clan-cli/.hypothesis/
ui-assets.tar.gz
ui-release

12
.gitlab-ci.yml Normal file
View 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

View File

@@ -13,6 +13,20 @@ clan webui --reload --no-open --log-level debug --populate --emulate
- 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
# 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
```
Run the image
```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
```
# API Documentation
Api documentation can be found in the folder `pkgs/clan-cli/tests/openapi_client/docs/`
@@ -24,12 +38,23 @@ For Entity object go to
- [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
# Building a Docker Image if the Frontend Changed
To build a docker image of the frontend and backend be inside the `pkgs/clan-cli` folder and execute:
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
nix build .#clan-docker
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:
@@ -44,13 +69,60 @@ And then run the docker file by executing:
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
# Uploading a Docker Image
## Docker build with UI changes
You can use the script:
If changes to the UI have been made, and you want them to propagate to the docker container edit the file: [../ui/nix/ui-assets.nix](../ui/nix/ui-assets.nix).
```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
@@ -60,14 +132,10 @@ nix build .#ui --out-link ui-release
Make a tarball out of it called `ui-assets.tar.gz`
```bash
tar -czvf ui-assets.tar.gz ui-release/lib/node_modules/clan-ui/out/
tar --transform 's,^\.,assets,' -czvf "ui-assets.tar.gz" -C ui-release/result/lib/node_modules/*/out .
```
Upload ui-assets.tar.gz to gitlab. 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. Then execute
```bash
export GITLAB_TOKEN="<your-access-token>"
```
Upload ui-assets.tar.gz to gitlab.
```bash
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
@@ -101,37 +169,30 @@ And now build the docker image:
nix build .#clan-docker
```
# Uploading a Docker Image
# Building a Docker Image if only the Backend Changed
Login to the tu docker image server
To build a new docker image only when the backend code changed execute:
```bash
docker login git.tu-berlin.de:5000
nix build .#clan-docker
```
Tag the imported image
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 image tag clan-docker:latest git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
docker load < result
```
Push the image to the git registry
And then run the docker file by executing:
```bash
docker image push git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
docker run -p 127.0.0.1:2979:2979 clan-docker:latest
```
Pull the image
```bash
docker pull git.tu-berlin.de:5000/internet-of-services-lab/service-aware-network-front-end:latest
```
Run the image
```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
```
- 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

16
pkgs/clan-cli/build_docker.sh Executable file
View 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

View File

@@ -5,7 +5,11 @@ cors_url = [
"http://0.0.0.0",
"http://[::]",
]
cors_ports = ["*", 3000, 2979]
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"

View File

@@ -6,6 +6,7 @@ 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
@@ -25,6 +26,14 @@ apps = [
(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 endpoints for different applications
@@ -97,11 +106,13 @@ async def consume_service_from_other_entity_c1() -> 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)
@@ -119,13 +130,15 @@ async def consume_service_from_other_entity_c2() -> HTMLResponse:
return HTMLResponse(content=html_content, status_code=200)
@app_c2.get("/v1/print_daemon1/register", response_class=JSONResponse)
@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_daemon1/deregister", response_class=JSONResponse)
@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)
@@ -138,32 +151,9 @@ async def ap_list_of_services() -> JSONResponse:
"uuid": "bdd640fb-0667-1ad1-1c80-317fa3b1799d",
"service_name": "Carlos Printing0",
"service_type": "3D Printing",
"endpoint_url": "http://127.0.0.1:8000/v1/print_daemon0",
"other": {},
"entity_did": "did:sov:test:120",
"status": {"data": ["draft", "registered"]},
"action": {
"data": [
{
"name": "register",
"endpoint": "http://127.0.0.1:8000/v1/print_daemon0/register",
},
{
"name": "deregister",
"endpoint": "http://127.0.0.1:8000/v1/print_daemon0/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:8001/v1/print_daemon1",
"other": {},
"entity_did": "did:sov:test:121",
"entity_did": "did:sov:test:120",
"status": {"data": ["draft", "registered"]},
"action": {
"data": [
@@ -179,13 +169,14 @@ async def ap_list_of_services() -> JSONResponse:
},
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
},
# Service 2 (similar structure)
{
"uuid": "bd9c66b3-ad3c-2d6d-1a3d-1fa7bc8960a9",
"service_name": "Carlos Printing2",
"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:122",
"entity_did": "did:sov:test:121",
"status": {"data": ["draft", "registered"]},
"action": {
"data": [
@@ -202,12 +193,12 @@ async def ap_list_of_services() -> JSONResponse:
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
},
{
"uuid": "972a8469-1641-9f82-8b9d-2434e465e150",
"service_name": "Carlos Printing3",
"uuid": "bd9c66b3-ad3c-2d6d-1a3d-1fa7bc8960a9",
"service_name": "Carlos Printing2",
"service_type": "3D Printing",
"endpoint_url": "http://127.0.0.1:8003/v1/print_daemon3",
"other": {},
"entity_did": "did:sov:test:123",
"entity_did": "did:sov:test:122",
"status": {"data": ["draft", "registered"]},
"action": {
"data": [
@@ -223,6 +214,28 @@ async def ap_list_of_services() -> JSONResponse:
},
"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"}],
},
]
return JSONResponse(content=res, status_code=200)

View File

@@ -1,11 +1,13 @@
import json
import logging
import time
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, JSONResponse
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
@@ -166,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)
@@ -360,37 +370,54 @@ def create_eventmessage(
@typing.no_type_check
@router.get(
"/api/v1/event_messages",
response_class=JSONResponse,
response_class=PlainTextResponse,
tags=[Tags.eventmessages],
)
def get_all_eventmessages(
skip: int = 0, limit: int = 100, db: Session = Depends(sql_db.get_db)
) -> JSONResponse:
) -> 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)
result: dict[int, dict[int, List[Eventmessage]]] = {}
cresult: List[OrderedDict[int, OrderedDict[int, List[Eventmessage]]]] = []
for msg in eventmessages:
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 = group.get("name", None) if group is not None else str(msg.group)
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 = src_name if src_name is None else src_name.name
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 = des_name if des_name is None else des_name.name
des_name = msg.des_did if des_name is None else des_name.name
# Initialize the result array and dictionary
if result.get(group_name) is None:
result[group_name] = {}
if result[group_name].get(msg.group_id) is None:
result[group_name][msg.group_id] = []
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[group_name][msg.group_id]
result_arr = result["groups"][msg.group_id]
result_arr.append(
Eventmessage(
id=msg.id,
@@ -408,9 +435,7 @@ def get_all_eventmessages(
).dict()
)
# sort by timestamp
result_arr.sort(key=lambda x: x["timestamp"])
return JSONResponse(content=result, status_code=200)
return PlainTextResponse(content=json.dumps(cresult, indent=4), status_code=200)
##############################

View File

@@ -1,7 +1,7 @@
# 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
@@ -319,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()
)

View File

@@ -101,7 +101,7 @@ class Eventmessage(Base):
## 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

9
pkgs/clan-cli/push_docker.sh Executable file
View 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

View File

@@ -40,7 +40,7 @@ def test_health(api_client: ApiClient) -> None:
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,6 +75,7 @@ def create_entities(num: int = 5, 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}",
@@ -113,7 +114,7 @@ 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 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
@@ -122,49 +123,49 @@ def test_create_services(api_client: ApiClient) -> None:
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)

View 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

View File

@@ -1,5 +1,5 @@
{ fetchzip }:
fetchzip {
url = "https://gitea.gchq.icu/api/packages/IoSL/generic/IoSL-service-aware-frontend/12ndzp04vy7xmqk90gakb4igy2qjf1pcfmr94r2cmpjrkkljdgbi/assets.tar.gz";
sha256 = "12ndzp04vy7xmqk90gakb4igy2qjf1pcfmr94r2cmpjrkkljdgbi";
url = "https://gitea.gchq.icu/api/packages/IoSL/generic/IoSL-service-aware-frontend/15svaig548jz1l8qsiqcycmw3hkb4805rb08mwlv2isxxshrj9ij/assets.tar.gz";
sha256 = "15svaig548jz1l8qsiqcycmw3hkb4805rb08mwlv2isxxshrj9ij";
}

View File

@@ -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>

View File

@@ -1,21 +0,0 @@
import Client from "@/app/client/client";
import { menuEntityEntries } from "@/components/sidebar";
export const dynamic = "error";
export const dynamicParams = false;
/*
The generateStaticParams function can be used in combination with dynamic route segments
to statically generate routes at build time instead of on-demand at request time.
During next dev, generateStaticParams will be called when you navigate to a route.
During next build, generateStaticParams runs before the corresponding Layouts or Pages are generated.
https://nextjs.org/docs/app/api-reference/functions/generate-static-params
*/
export function generateStaticParams() {
return menuEntityEntries.map((entry) => ({
name: entry.label,
}));
}
export default function Page({ params }: { params: { name: string } }) {
return <Client params={params} />;
}

View File

@@ -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,8 +104,14 @@ const AttachButton = ({
);
};
export default function Client({ params }: { params: { name: string } }) {
const { 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(name);
const {
@@ -133,24 +138,21 @@ export default function Client({ params }: { params: { name: string } }) {
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);
};
console.log("entity", entity);
// Consume
const handleConsumeContent = (content: any) => {
setConsumeContent(content);
};
if (services_loading) return <Skeleton height={500} />;
if (!services) return <Alert severity="error">Client not found</Alert>;
@@ -178,34 +180,43 @@ export default function Client({ params }: { params: { name: string } }) {
</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?.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}

View File

@@ -0,0 +1,5 @@
import Client from "@/app/client/client";
export default function Page() {
return <Client />;
}

View File

@@ -1,13 +1,16 @@
"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 { 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,
isLoading: loadingResolutions,
@@ -28,7 +31,7 @@ export default function DLG() {
useEffect(() => {
const interval = setInterval(() => {
onRefresh();
}, 5000);
}, projectConfig.REFRESH_FREQUENCY);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -37,12 +40,24 @@ 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>

View File

@@ -8,6 +8,7 @@ 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"),
@@ -30,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
@@ -42,7 +43,6 @@ export default function Home() {
entity={{ name: "Home", details: [] }}
hasRefreshButton={true}
onRefresh={onRefresh}
hasAttachDetach={false}
/>
<div>

View File

@@ -51,7 +51,6 @@ 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 src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> */}
<script
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{

View 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;

View File

@@ -0,0 +1,9 @@
const ConsumeDisplayComponent = ({ htmlContent }: { htmlContent: any }) => {
return (
<div>
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
</div>
);
};
export default ConsumeDisplayComponent;

View File

@@ -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;

View 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;
```

View 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;

View 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;

View File

@@ -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;

View File

@@ -1,136 +1,93 @@
import { Eventmessage } from "@/api/model";
import { getGroupColor, sanitizeDID } from "@/utils/helpers";
export const generateMermaidString = (data: Eventmessage[] | undefined) => {
if (!data || data.length === 0) return "";
const participants = Array.from(
new Set(data.flatMap((item) => [item.src_did, item.des_did])),
);
export const generateMermaidString = (data: any) => {
if (!data || !data.length) return "";
let mermaidString = "sequenceDiagram\n";
const participantDetails = new Map();
participants.forEach((participant, index) => {
mermaidString += ` participant ${String.fromCharCode(
65 + index,
)} as ${participant}\n`;
// 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));
});
});
});
let currentGroupId: number | null = null;
data.forEach((item, index) => {
const srcParticipant = String.fromCharCode(
65 + participants.indexOf(item.src_did),
);
const desParticipant = String.fromCharCode(
65 + participants.indexOf(item.des_did),
);
const timestamp = new Date(item.timestamp * 1000).toLocaleString();
const message = item.msg.text || `Event message ${index + 1}`;
if (item.group_id !== currentGroupId) {
if (currentGroupId !== null) {
mermaidString += ` end\n`;
}
mermaidString += ` alt Group ${item.group_id}\n`;
currentGroupId = item.group_id;
}
mermaidString += ` ${srcParticipant}->>${desParticipant}: [${timestamp}] ${message}\n`;
// Add participants to the mermaid string with names and sanitized DIDs
participantDetails.forEach((sanitizedDID, name) => {
mermaidString += ` participant ${name} as ${name} <br/>${sanitizedDID}\n`;
});
if (currentGroupId !== null) {
mermaidString += ` end\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;
};
// Dummy Data
export function extractAllEventMessages(data: any) {
const allMessagesArray: any = [];
export const dataFromBE = [
{
id: 12,
timestamp: 1704892813,
group: 0,
group_id: 12,
// "group_name": "Data",
msg_type: 4,
src_did: "did:sov:test:121",
// "src_name": "Entity A",
des_did: "did:sov:test:120",
// "des_name": "Entity B",
msg: {
text: "Hello World",
},
},
{
id: 60,
timestamp: 1704892823,
group: 1,
group_id: 19,
msg_type: 4,
src_did: "did:sov:test:122",
des_did: "did:sov:test:121",
msg: {},
},
{
id: 30162,
timestamp: 1704892817,
group: 1,
group_id: 53,
msg_type: 2,
src_did: "did:sov:test:121",
des_did: "did:sov:test:122",
msg: {},
},
{
id: 63043,
timestamp: 1704892809,
group: 0,
group_id: 12,
msg_type: 3,
src_did: "did:sov:test:121",
des_did: "did:sov:test:120",
msg: {},
},
{
id: 66251,
timestamp: 1704892805,
group: 0,
group_id: 51,
msg_type: 1,
src_did: "did:sov:test:120",
des_did: "did:sov:test:121",
msg: {},
},
{
id: 85434,
timestamp: 1704892807,
group: 0,
group_id: 51,
msg_type: 2,
src_did: "did:sov:test:120",
des_did: "did:sov:test:121",
msg: {},
},
{
id: 124842,
timestamp: 1704892819,
group: 1,
group_id: 19,
msg_type: 3,
src_did: "did:sov:test:122",
des_did: "did:sov:test:121",
msg: {},
},
{
id: 246326,
timestamp: 1704892815,
group: 1,
group_id: 53,
msg_type: 1,
src_did: "did:sov:test:121",
des_did: "did:sov:test:122",
msg: {},
},
];
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;
}

View File

@@ -2,48 +2,94 @@
import { useRef, useEffect, useState } from "react";
import mermaid from "mermaid";
import { IconButton } from "@mui/material";
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 Tooltip from "@mui/material/Tooltip";
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 { LoadingOverlay } from "../join/loadingOverlay";
//import { generateMermaidString } from "./helpers";
import { extractAllEventMessages, generateMermaidString } from "./helpers";
import CopyToClipboard from "../copy_to_clipboard";
import { formatDateTime, getGroupById } from "@/utils/helpers";
const SequenceDiagram = () => {
const {
// data: eventMessagesData,
data: eventMessagesData,
isLoading: loadingEventMessages,
swrKey: eventMessagesKeyFunc,
} = useGetAllEventmessages();
const mermaidRef: any = useRef(null);
const [scale, setScale] = useState(1);
const hasData = false; // TODO: Readd this, right now it's always false
const [openFilters, setOpenFilters] = useState(false);
const [sequenceNr, setSequenceNr] = useState("");
const mermaidString = ""; //generateMermaidString(eventMessagesData?.data);
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(() => {
if (!loadingEventMessages && hasData)
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
sequence: {
mirrorActors: false,
},
});
const currentMermaidRef = mermaidRef?.current;
if (mermaidRef.current) {
mermaidRef.current.innerHTML = mermaidString;
mermaid.init(undefined, 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);
}
}
}, [loadingEventMessages, hasData, mermaidString]);
return () => {
if (currentMermaidRef) {
currentMermaidRef.removeAttribute("data-processed");
currentMermaidRef.innerHTML = "";
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataDependency]);
useEffect(() => {
if (mermaidRef.current) {
@@ -126,55 +172,187 @@ const SequenceDiagram = () => {
}
};
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 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 && (
<>
<div className="flex justify-end">
<Tooltip placement="top" title="Refresh Diagram">
<IconButton color="default" onClick={onRefresh}>
<RefreshIcon />
</IconButton>
</Tooltip>
<Tooltip title="Zoom In" placement="top">
<IconButton color="primary" onClick={zoomIn}>
<ZoomInIcon />
</IconButton>
</Tooltip>
<Tooltip title="Zoom Out" placement="top">
<IconButton color="primary" onClick={zoomOut}>
<ZoomOutIcon />
</IconButton>
</Tooltip>
<Tooltip title="Reset" placement="top">
<IconButton color="primary" onClick={resetZoom}>
<ResetIcon />
</IconButton>
</Tooltip>
<Tooltip title="View in Fullscreen" placement="top">
<IconButton color="primary" onClick={viewInFullScreen}>
<FullscreenIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download as PNG" placement="top">
<IconButton color="primary" onClick={downloadAsPng}>
<DownloadIcon />
</IconButton>
</Tooltip>
</div>
<div className="w-full p-2.5">
<div className="mermaid" ref={mermaidRef}></div>
</div>
<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>
</>
) : (
<div className="flex w-full justify-center">
<NoDataOverlay label="No Activity yet" />
</div>
)}
</div>
</>
);
};

View File

@@ -9,6 +9,8 @@ import {
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";
@@ -33,33 +35,6 @@ type MenuEntry = {
subMenuEntries?: MenuEntry[];
};
export const menuEntityEntries: MenuEntry[] = [
{
icon: <PersonIcon />,
label: "C1",
to: "/client/C1",
disabled: false,
},
{
icon: <PersonIcon />,
label: "C2",
to: "/client/C2",
disabled: false,
},
{
icon: <PersonIcon />,
label: "C3",
to: "/client/C3",
disabled: false,
},
{
icon: <PersonIcon />,
label: "C4",
to: "/client/C4",
disabled: false,
},
];
export const menuEntries: MenuEntry[] = [
{
icon: <HomeIcon />,
@@ -96,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 : "",
@@ -112,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
@@ -213,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>

View File

@@ -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}>

View 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
}}
/>
```

View File

@@ -12,7 +12,13 @@ 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} />;
@@ -23,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;
@@ -35,10 +46,17 @@ 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 (typeof renderedValue === "object" && render === undefined) {
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 (
<ErrorBoundary>
<StyledTableCell key={cellKey} align="left">
@@ -59,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,
);
},
)}

View 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;
```

View File

@@ -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,
},

View File

@@ -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 = [
{
@@ -61,29 +46,9 @@ export const APServiceRepositoryTableConfig = [
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);
}
if (Array.isArray(value.data)) renderedValue = value.data.join(", ");
else console.error("Status is not an array", value);
return renderedValue;
},
},
// {
// 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>
// );
// }
// return renderedValue;
// },
// },
];

View File

@@ -1,7 +1,5 @@
import { Button, IconButton, Tooltip } from "@mui/material";
import AddCircleIcon from "@mui/icons-material/AddCircle";
import RemoveCircleIcon from "@mui/icons-material/RemoveCircle";
import DeleteIcon from "@mui/icons-material/Delete";
import EntityActions from "@/components/entity_actions";
import ConsumeAction from "@/components/consume_action";
export const ClientTableConfig = [
{
@@ -15,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}
/>
);
},
},
@@ -88,39 +88,10 @@ export const ServiceTableConfig = [
{
key: "action",
label: "Actions",
render: () => {
return (
<>
<Tooltip title="Register" placement="top">
<IconButton disabled size="small">
<AddCircleIcon />
</IconButton>
</Tooltip>
<Tooltip title="De-register" placement="top">
<IconButton disabled size="small">
<RemoveCircleIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete" placement="top">
<IconButton disabled size="small" color="secondary">
<DeleteIcon />
</IconButton>
</Tooltip>
</>
);
// let renderedValue: any = "";
// if (typeof value === "object")
// renderedValue = (
// <>
// {[...value.data, { name: 'Delete', endpoint: '' }].map((actionType: any) => (
// <>
// <Button disabled style={{ marginRight: 8 }} variant="outlined" size="small">{actionType.name}</Button>
// </>
// ))}
// </>
// );
// return renderedValue;
render: (value: any, rowData?: any) => {
if (value && value?.data.length > 0)
return <EntityActions rowData={rowData} endpointData={value.data} />;
else return "N/A";
},
},
];

View 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" },
],
},
],
};

View File

@@ -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 = [
{

View File

@@ -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 };

View File

@@ -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;
}

View File

@@ -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 : {};
}