Compare commits

...

58 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
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
35 changed files with 1321 additions and 503 deletions

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

@@ -5,7 +5,11 @@ cors_url = [
"http://0.0.0.0", "http://0.0.0.0",
"http://[::]", "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 for the server, frontend, backend and emulators
host = "127.0.0.1" host = "127.0.0.1"

View File

@@ -6,6 +6,7 @@ from datetime import datetime
# Importing FastAPI and related components # Importing FastAPI and related components
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
# Importing configuration and schemas from the clan_cli package # Importing configuration and schemas from the clan_cli package
@@ -25,6 +26,14 @@ apps = [
(app_c1, config.c1_port), (app_c1, config.c1_port),
(app_c2, config.c2_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 # 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) @app_c1.get("/v1/print_daemon1/register", response_class=JSONResponse)
async def register_c1() -> JSONResponse: async def register_c1() -> JSONResponse:
time.sleep(2)
return JSONResponse(content={"status": "registered"}, status_code=200) return JSONResponse(content={"status": "registered"}, status_code=200)
@app_c1.get("/v1/print_daemon1/deregister", response_class=JSONResponse) @app_c1.get("/v1/print_daemon1/deregister", response_class=JSONResponse)
async def deregister_c1() -> JSONResponse: async def deregister_c1() -> JSONResponse:
time.sleep(2)
return JSONResponse(content={"status": "deregistered"}, status_code=200) 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) 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: async def register_c2() -> JSONResponse:
time.sleep(2)
return JSONResponse(content={"status": "registered"}, status_code=200) 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: async def deregister_c2() -> JSONResponse:
time.sleep(2)
return JSONResponse(content={"status": "deregistered"}, status_code=200) 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", "uuid": "bdd640fb-0667-1ad1-1c80-317fa3b1799d",
"service_name": "Carlos Printing0", "service_name": "Carlos Printing0",
"service_type": "3D Printing", "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", "endpoint_url": "http://127.0.0.1:8001/v1/print_daemon1",
"other": {}, "other": {},
"entity_did": "did:sov:test:121", "entity_did": "did:sov:test:120",
"status": {"data": ["draft", "registered"]}, "status": {"data": ["draft", "registered"]},
"action": { "action": {
"data": [ "data": [
@@ -179,13 +169,14 @@ async def ap_list_of_services() -> JSONResponse:
}, },
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}], "usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
}, },
# Service 2 (similar structure)
{ {
"uuid": "bd9c66b3-ad3c-2d6d-1a3d-1fa7bc8960a9", "uuid": "23b8c1e9-3924-56de-3eb1-3b9046685257",
"service_name": "Carlos Printing2", "service_name": "Carlos Printing1",
"service_type": "3D Printing", "service_type": "3D Printing",
"endpoint_url": "http://127.0.0.1:8002/v1/print_daemon2", "endpoint_url": "http://127.0.0.1:8002/v1/print_daemon2",
"other": {}, "other": {},
"entity_did": "did:sov:test:122", "entity_did": "did:sov:test:121",
"status": {"data": ["draft", "registered"]}, "status": {"data": ["draft", "registered"]},
"action": { "action": {
"data": [ "data": [
@@ -202,12 +193,12 @@ async def ap_list_of_services() -> JSONResponse:
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}], "usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}],
}, },
{ {
"uuid": "972a8469-1641-9f82-8b9d-2434e465e150", "uuid": "bd9c66b3-ad3c-2d6d-1a3d-1fa7bc8960a9",
"service_name": "Carlos Printing3", "service_name": "Carlos Printing2",
"service_type": "3D Printing", "service_type": "3D Printing",
"endpoint_url": "http://127.0.0.1:8003/v1/print_daemon3", "endpoint_url": "http://127.0.0.1:8003/v1/print_daemon3",
"other": {}, "other": {},
"entity_did": "did:sov:test:123", "entity_did": "did:sov:test:122",
"status": {"data": ["draft", "registered"]}, "status": {"data": ["draft", "registered"]},
"action": { "action": {
"data": [ "data": [
@@ -223,6 +214,28 @@ async def ap_list_of_services() -> JSONResponse:
}, },
"usage": [{"times_consumed": 2, "consumer_entity_did": "did:sov:test:120"}], "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) return JSONResponse(content=res, status_code=200)

View File

@@ -168,6 +168,14 @@ def get_entity_by_roles(
return entity 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]) @router.get("/api/v1/entities", response_model=List[Entity], tags=[Tags.entities])
def get_all_entities( def get_all_entities(
skip: int = 0, limit: int = 100, db: Session = Depends(sql_db.get_db) skip: int = 0, limit: int = 100, db: Session = Depends(sql_db.get_db)
@@ -386,9 +394,9 @@ def get_all_eventmessages(
# Get the name of the src and des entity from the database # 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 = 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 = 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
result = cresult[cresult_idx] result = cresult[cresult_idx]

View File

@@ -40,7 +40,7 @@ def test_health(api_client: ApiClient) -> None:
def create_entities(num: int = 5, role: str = "entity") -> list[EntityCreate]: def create_entities(num: int = 5, role: str = "entity") -> list[EntityCreate]:
res = [] res = []
for i in range(num): for i in range(1, num + 1):
en = EntityCreate( en = EntityCreate(
did=f"did:sov:test:12{i}", did=f"did:sov:test:12{i}",
name=f"C{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: def create_service(idx: int, entity: Entity) -> ServiceCreate:
idx += 1
se = ServiceCreate( se = ServiceCreate(
uuid=uuids[idx], uuid=uuids[idx],
service_name=f"Carlos Printing{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: def test_create_services(api_client: ApiClient) -> None:
sapi = ServicesApi(api_client=api_client) sapi = ServicesApi(api_client=api_client)
eapi = EntitiesApi(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_obj = create_service(midx, entity)
service = sapi.create_service(service_obj) service = sapi.create_service(service_obj)
assert service.uuid == service_obj.uuid assert service.uuid == service_obj.uuid
@@ -125,8 +126,7 @@ random.seed(77)
def create_eventmessages(num: int = 4) -> list[EventmessageCreate]: def create_eventmessages(num: int = 4) -> list[EventmessageCreate]:
res = [] res = []
starttime = int(time.time()) starttime = int(time.time())
for idx in range(num): for i2 in range(1, num + 1):
i2 = idx + 1
group_id = i2 % 5 + random.getrandbits(6) + 1 group_id = i2 % 5 + random.getrandbits(6) + 1
em_req_send = EventmessageCreate( em_req_send = EventmessageCreate(
timestamp=starttime + i2 * 10, timestamp=starttime + i2 * 10,

View File

@@ -1,5 +1,5 @@
{ fetchzip }: { fetchzip }:
fetchzip { fetchzip {
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"; url = "https://gitea.gchq.icu/api/packages/IoSL/generic/IoSL-service-aware-frontend/15svaig548jz1l8qsiqcycmw3hkb4805rb08mwlv2isxxshrj9ij/assets.tar.gz";
sha256 = "1npc1f4pa2d0q16bdygbclnx0c2lsi4j1w8r708xc35s1lvn1a4y"; sha256 = "15svaig548jz1l8qsiqcycmw3hkb4805rb08mwlv2isxxshrj9ij";
} }

View File

@@ -6,13 +6,15 @@ import { useGetAllRepositories } from "@/api/repositories/repositories";
import SummaryDetails from "@/components/summary_card"; import SummaryDetails from "@/components/summary_card";
import CustomTable from "@/components/table"; import CustomTable from "@/components/table";
import { import {
APSummaryDetails,
APAttachmentsTableConfig, APAttachmentsTableConfig,
APServiceRepositoryTableConfig, APServiceRepositoryTableConfig,
} from "@/config/access_point"; } from "@/config/access_point";
import { useEffect } from "react"; import { useEffect } from "react";
import useGetEntityByNameOrDid from "@/components/hooks/useGetEntityByNameOrDid";
import { projectConfig } from "@/config/config";
export default function AccessPoint() { export default function AccessPoint() {
const { entity } = useGetEntityByNameOrDid("AP");
const { const {
data: APAttachementData, data: APAttachementData,
isLoading: loadingAttachements, isLoading: loadingAttachements,
@@ -45,7 +47,7 @@ export default function AccessPoint() {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
onRefresh(); onRefresh();
}, 5000); }, projectConfig.REFRESH_FREQUENCY);
return () => clearInterval(interval); return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -54,10 +56,25 @@ export default function AccessPoint() {
return ( return (
<div className="m-10"> <div className="m-10">
<SummaryDetails <SummaryDetails
fake
hasRefreshButton hasRefreshButton
onRefresh={onRefresh} 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> <div>
<h4>Attachment View</h4> <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"; "use client";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { ClientTableConfig, ServiceTableConfig } from "@/config/client_1"; import { ClientTableConfig, ServiceTableConfig } from "@/config/client_1";
import CustomTable from "@/components/table"; import CustomTable from "@/components/table";
import { import {
Alert, Alert,
Button, Button,
Card,
CardContent,
CardHeader,
Snackbar, Snackbar,
Typography,
CircularProgress, CircularProgress,
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import CopyToClipboard from "@/components/copy_to_clipboard";
import { import {
attachEntity, attachEntity,
detachEntity, detachEntity,
@@ -26,6 +21,10 @@ import useGetEntityByNameOrDid from "@/components/hooks/useGetEntityByNameOrDid"
import { useGetAllServices } from "@/api/services/services"; import { useGetAllServices } from "@/api/services/services";
import axios from "axios"; import axios from "axios";
import CloseIcon from "@mui/icons-material/Close"; 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 { interface SnackMessage {
message: string; message: string;
@@ -105,8 +104,14 @@ const AttachButton = ({
); );
}; };
export default function Client({ params }: { params: { name: string } }) { export default function Client() {
const { name } = params; 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 { entity: entity } = useGetEntityByNameOrDid(name);
const { const {
@@ -133,24 +138,21 @@ export default function Client({ params }: { params: { name: string } }) {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
onRefresh(); onRefresh();
}, 5000); }, projectConfig.REFRESH_FREQUENCY);
return () => clearInterval(interval); return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 = () => { const closeSnackBar = () => {
setSnackbarMessage(undefined); setSnackbarMessage(undefined);
setSnackbarOpen(false); setSnackbarOpen(false);
}; };
console.log("entity", entity); // Consume
const handleConsumeContent = (content: any) => {
setConsumeContent(content);
};
if (services_loading) return <Skeleton height={500} />; if (services_loading) return <Skeleton height={500} />;
if (!services) return <Alert severity="error">Client not found</Alert>; if (!services) return <Alert severity="error">Client not found</Alert>;
@@ -178,34 +180,43 @@ export default function Client({ params }: { params: { name: string } }) {
</div> </div>
</div> </div>
<Card variant="outlined"> <SummaryDetails
<CardHeader entity={{
subheader="Summary" name: "",
action={<CopyToClipboard contentRef={cardContentRef} />} details: [
/> { label: "DID", value: entity?.did },
<CardContent ref={cardContentRef}> { label: "IP", value: entity?.ip },
<Typography color="text.primary" gutterBottom> { label: "Network", value: entity?.network },
DID: <code>{entity?.did}</code> ],
</Typography> }}
<Typography color="text.primary" gutterBottom> />
IP: <code>{entity?.ip}</code> <div
</Typography> style={{
<Typography color="text.primary" gutterBottom> display: "flex",
Network: <code>{entity?.network}</code> justifyContent: "space-between",
</Typography> flexWrap: "nowrap",
</CardContent> alignItems: "center",
</Card> }}
<div> >
<h4>Client View</h4> <div style={{ width: consumeContent ? "55%" : "100%" }}>
<CustomTable <h4>Service Consumer View</h4>
loading={services_loading} <CustomTable
data={clients} loading={services_loading}
configuration={ClientTableConfig} data={clients}
tkey="client-table" onConsumeAction={handleConsumeContent}
/> configuration={ClientTableConfig}
tkey="client-table"
/>
</div>
{consumeContent && (
<div style={{ width: "40%" }}>
<h4>Service Output</h4>
<ConsumeDisplayComponent htmlContent={consumeContent} />
</div>
)}
</div> </div>
<div> <div>
<h4>Service View</h4> <h4>Service Producer View</h4>
<CustomTable <CustomTable
loading={services_loading} loading={services_loading}
data={services?.data} 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"; "use client";
import { DLGResolutionTableConfig, DLGSummaryDetails } from "@/config/dlg"; import { DLGResolutionTableConfig } from "@/config/dlg";
import CustomTable from "@/components/table"; import CustomTable from "@/components/table";
import SummaryDetails from "@/components/summary_card"; import SummaryDetails from "@/components/summary_card";
import { useEffect } from "react"; import { useEffect } from "react";
import { useGetAllResolutions } from "@/api/resolution/resolution"; import { useGetAllResolutions } from "@/api/resolution/resolution";
import { mutate } from "swr"; import { mutate } from "swr";
import useGetEntityByNameOrDid from "@/components/hooks/useGetEntityByNameOrDid";
import { projectConfig } from "@/config/config";
export default function DLG() { export default function DLG() {
const { entity } = useGetEntityByNameOrDid("DLG");
const { const {
data: resolutionData, data: resolutionData,
isLoading: loadingResolutions, isLoading: loadingResolutions,
@@ -28,7 +31,7 @@ export default function DLG() {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
onRefresh(); onRefresh();
}, 5000); }, projectConfig.REFRESH_FREQUENCY);
return () => clearInterval(interval); return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -37,12 +40,24 @@ export default function DLG() {
return ( return (
<div className="m-10"> <div className="m-10">
<SummaryDetails <SummaryDetails
fake
hasRefreshButton hasRefreshButton
onRefresh={onRefresh} onRefresh={onRefresh}
entity={{ entity={{
name: "Distributed Ledger Gateway", name: "Distributed Ledger Gateway",
details: DLGSummaryDetails, details: [
{
label: "DID",
value: entity?.did,
},
{
label: "IP",
value: entity?.ip,
},
{
label: "Network",
value: entity?.network,
},
],
}} }}
/> />
<div> <div>

View File

@@ -8,6 +8,7 @@ import dynamic from "next/dynamic";
import { useEffect } from "react"; import { useEffect } from "react";
import { mutate } from "swr"; import { mutate } from "swr";
import ErrorBoundary from "@/components/error_boundary"; import ErrorBoundary from "@/components/error_boundary";
import { projectConfig } from "@/config/config";
const NoSSRSequenceDiagram = dynamic( const NoSSRSequenceDiagram = dynamic(
() => import("../../components/sequence_diagram"), () => import("../../components/sequence_diagram"),
@@ -30,7 +31,7 @@ export default function Home() {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
onRefresh(); onRefresh();
}, 5000); }, projectConfig.REFRESH_FREQUENCY);
return () => clearInterval(interval); return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -42,7 +43,6 @@ export default function Home() {
entity={{ name: "Home", details: [] }} entity={{ name: "Home", details: [] }}
hasRefreshButton={true} hasRefreshButton={true}
onRefresh={onRefresh} onRefresh={onRefresh}
hasAttachDetach={false}
/> />
<div> <div>

View File

@@ -51,7 +51,6 @@ export default function RootLayout({
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Service Aware Networks" /> <meta name="description" content="Service Aware Networks" />
<link rel="icon" href="tub-favicon.ico" sizes="any" /> <link rel="icon" href="tub-favicon.ico" sizes="any" />
{/* <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script> */}
<script <script
// eslint-disable-next-line react/no-danger // eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ 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 { useState, RefObject } from "react";
import { Button, Snackbar } from "@mui/material"; 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 [open, setOpen] = useState(false);
const handleClick = () => { const handleClick = () => {
if (contentRef.current) { // Prioritize direct text copy if 'textToCopy' is provided
const text = contentRef.current.textContent; const text = textToCopy || contentRef?.current?.textContent || "";
navigator.clipboard.writeText(text); const copiedText = textToCopy ? JSON.stringify(text, null, 2) : text;
setOpen(true);
if (text) {
navigator.clipboard.writeText(copiedText).then(
() => {
setOpen(true);
},
(err) => {
console.error("Could not copy text: ", err);
},
);
} }
}; };
return ( return (
<> <>
<Button onClick={handleClick}>Copy</Button> <Tooltip placement="left" title="Copy to Clipboard">
<ContentCopyIcon onClick={handleClick} className="cursor-pointer" />
</Tooltip>
<Snackbar <Snackbar
open={open} open={open}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
autoHideDuration={2000} autoHideDuration={2000}
message="Copied to clipboard" message="Copied to clipboard!"
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
/> />
</> </>
); );
}; };
export default CopyToClipboard; 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) => { export const generateMermaidString = (data: any) => {
if (!data || data.length === 0) return ""; if (!data || !data.length) return "";
const participants = Array.from(
new Set(data.flatMap((item) => [item.src_did, item.des_did])),
);
let mermaidString = "sequenceDiagram\n"; let mermaidString = "sequenceDiagram\n";
const participantDetails = new Map();
participants.forEach((participant, index) => { // Collect all unique participants along with their sanitized DIDs
mermaidString += ` participant ${String.fromCharCode( data.forEach((item: any) => {
65 + index, Object.values(item.groups).forEach((group: any) => {
)} as ${participant}\n`; 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; // Add participants to the mermaid string with names and sanitized DIDs
participantDetails.forEach((sanitizedDID, name) => {
data.forEach((item, index) => { mermaidString += ` participant ${name} as ${name} <br/>${sanitizedDID}\n`;
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`;
}); });
if (currentGroupId !== null) { // Iterate through each group
mermaidString += ` end\n`; 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; return mermaidString;
}; };
// Dummy Data export function extractAllEventMessages(data: any) {
const allMessagesArray: any = [];
export const dataFromBE = [ if (!data || data.length === 0) return allMessagesArray;
{ else
id: 12, data.forEach((groupData: any) => {
timestamp: 1704892813, Object.values(groupData.groups).forEach((messages: any) => {
group: 0, messages.forEach((message: any) => {
group_id: 12, allMessagesArray.push(message);
// "group_name": "Data", });
msg_type: 4, });
src_did: "did:sov:test:121", });
// "src_name": "Entity A", return allMessagesArray;
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: {},
},
];

View File

@@ -2,48 +2,94 @@
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import mermaid from "mermaid"; 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 RefreshIcon from "@mui/icons-material/Refresh";
import ZoomInIcon from "@mui/icons-material/ZoomIn"; import ZoomInIcon from "@mui/icons-material/ZoomIn";
import ZoomOutIcon from "@mui/icons-material/ZoomOut"; import ZoomOutIcon from "@mui/icons-material/ZoomOut";
import FullscreenIcon from "@mui/icons-material/Fullscreen"; import FullscreenIcon from "@mui/icons-material/Fullscreen";
import DownloadIcon from "@mui/icons-material/Download"; import DownloadIcon from "@mui/icons-material/Download";
import ResetIcon from "@mui/icons-material/Autorenew"; 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 { NoDataOverlay } from "../noDataOverlay";
import { LoadingOverlay } from "../join/loadingOverlay";
import { useGetAllEventmessages } from "@/api/eventmessages/eventmessages"; import { useGetAllEventmessages } from "@/api/eventmessages/eventmessages";
import { mutate } from "swr"; 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 SequenceDiagram = () => {
const { const {
// data: eventMessagesData, data: eventMessagesData,
isLoading: loadingEventMessages, isLoading: loadingEventMessages,
swrKey: eventMessagesKeyFunc, swrKey: eventMessagesKeyFunc,
} = useGetAllEventmessages(); } = useGetAllEventmessages();
const mermaidRef: any = useRef(null);
const [scale, setScale] = useState(1); 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(() => { useEffect(() => {
if (!loadingEventMessages && hasData) const currentMermaidRef = mermaidRef?.current;
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
sequence: {
mirrorActors: false,
},
});
if (mermaidRef.current) { if (!loadingEventMessages && hasData) {
mermaidRef.current.innerHTML = mermaidString; if (
mermaid.init(undefined, mermaidRef.current); 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(() => { useEffect(() => {
if (mermaidRef.current) { 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) if (loadingEventMessages)
return <LoadingOverlay title="Loading Diagram" subtitle="Please wait..." />; return <LoadingOverlay title="Loading Diagram" subtitle="Please wait..." />;
return ( 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"> <Dialog
<Tooltip placement="top" title="Refresh Diagram"> open={openFilters}
<IconButton color="default" onClick={onRefresh}> keepMounted
<RefreshIcon /> fullWidth
</IconButton> maxWidth="lg"
</Tooltip> onClose={toggleFilters}
<Tooltip title="Zoom In" placement="top"> >
<IconButton color="primary" onClick={zoomIn}> <DialogTitle>All Event Messages</DialogTitle>
<ZoomInIcon /> <DialogContent>
</IconButton> <DialogContentText>
</Tooltip> <div className="flex items-center gap-2.5">
<Tooltip title="Zoom Out" placement="top"> <label>Search by Sequence # </label>
<IconButton color="primary" onClick={zoomOut}> <TextField
<ZoomOutIcon /> onChange={onSearchBySeqNumber}
</IconButton> size="small"
</Tooltip> variant="outlined"
<Tooltip title="Reset" placement="top"> />
<IconButton color="primary" onClick={resetZoom}> </div>
<ResetIcon /> <List className="w-full" component="nav">
</IconButton> {allEventMessages
</Tooltip> .filter((_: any, index: number) => {
<Tooltip title="View in Fullscreen" placement="top"> return isFilterMatch(index);
<IconButton color="primary" onClick={viewInFullScreen}> })
<FullscreenIcon /> .map((message: any, index: number) => {
</IconButton> const {
</Tooltip> msg_type_name: msgType,
<Tooltip title="Download as PNG" placement="top"> des_name,
<IconButton color="primary" onClick={downloadAsPng}> src_name,
<DownloadIcon /> group,
</IconButton> group_id,
</Tooltip> timestamp,
</div> src_did,
<div className="w-full p-2.5"> des_did,
<div className="mermaid" ref={mermaidRef}></div> // msg, TODO: Need to use the content inside the msg to display in the diagram
</div> } = 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,7 +9,8 @@ import {
Tooltip, Tooltip,
useMediaQuery, useMediaQuery,
} from "@mui/material"; } from "@mui/material";
import { useGetAllEntities } from "@/api/entities/entities"; import { useGetEntityByRole } from "@/api/entities/entities";
import { Role } from "@/api/model/role";
import Image from "next/image"; import Image from "next/image";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
@@ -34,8 +35,6 @@ type MenuEntry = {
subMenuEntries?: MenuEntry[]; subMenuEntries?: MenuEntry[];
}; };
export let menuEntityEntries: MenuEntry[] = [];
export const menuEntries: MenuEntry[] = [ export const menuEntries: MenuEntry[] = [
{ {
icon: <HomeIcon />, icon: <HomeIcon />,
@@ -72,7 +71,9 @@ interface SidebarProps {
} }
export function Sidebar(props: SidebarProps) { export function Sidebar(props: SidebarProps) {
const { data: entityData } = useGetAllEntities(); const { data: entityData } = useGetEntityByRole({
role: Role.service_prosumer,
});
const { show, onClose } = props; const { show, onClose } = props;
const [activeMenuItem, setActiveMenuItem] = React.useState( const [activeMenuItem, setActiveMenuItem] = React.useState(
typeof window !== "undefined" ? window.location.pathname : "", typeof window !== "undefined" ? window.location.pathname : "",
@@ -89,17 +90,22 @@ export function Sidebar(props: SidebarProps) {
setCollapseMenuOpen(!collapseMenuOpen); setCollapseMenuOpen(!collapseMenuOpen);
}; };
React.useEffect(() => { const menuEntityEntries: MenuEntry[] = React.useMemo(() => {
if (entityData) { if (entityData) {
menuEntityEntries = Array.isArray(entityData.data) return Array.isArray(entityData.data)
? entityData.data.map((entity) => ({ ? entityData.data.map((entity: any) => ({
icon: <PersonIcon />, icon: <PersonIcon />,
label: entity.name, label: entity.name,
to: `/client/${entity.name}`, to: entity.name,
disabled: false, disabled: false,
})) }))
: []; : [];
} else {
return [];
} }
}, [entityData]);
React.useEffect(() => {
if (isSmallerScreen) { if (isSmallerScreen) {
setCollapseMenuOpen(false); setCollapseMenuOpen(false);
} else { } else {
@@ -201,30 +207,35 @@ export function Sidebar(props: SidebarProps) {
> >
<List component="div" disablePadding> <List component="div" disablePadding>
{menuEntityEntries?.map((menuEntry, idx) => ( {menuEntityEntries?.map((menuEntry, idx) => (
<ListItemButton <Link
key={idx} key={"entity-link-" + idx}
sx={{ pl: 4 }} href={`/client?name=${menuEntry.to}`}
className="lg:justify-normal" style={{ textDecoration: "none", color: "white" }}
LinkComponent={Link}
href={menuEntry.to}
disabled={menuEntry.disabled}
selected={activeMenuItem === menuEntry.to}
onClick={() => handleMenuItemClick(menuEntry.to)}
> >
<ListItemIcon <ListItemButton
color="inherit" key={idx}
className="overflow-hidden text-white lg:justify-normal" sx={{ pl: 4 }}
className="lg:justify-normal"
LinkComponent={Link}
disabled={menuEntry.disabled}
selected={activeMenuItem === menuEntry.to}
onClick={() => handleMenuItemClick(menuEntry.to)}
> >
{menuEntry.icon} <ListItemIcon
</ListItemIcon> color="inherit"
<ListItemText className="overflow-hidden text-white lg:justify-normal"
primary={menuEntry.label} >
primaryTypographyProps={{ {menuEntry.icon}
color: "inherit", </ListItemIcon>
}} <ListItemText
className="hidden lg:block" primary={menuEntry.label}
/> primaryTypographyProps={{
</ListItemButton> color: "inherit",
}}
className="hidden lg:block"
/>
</ListItemButton>
</Link>
))} ))}
</List> </List>
</Collapse> </Collapse>

View File

@@ -13,29 +13,17 @@ import { EntityDetails, ISummaryDetails } from "@/types";
const SummaryDetails = ({ const SummaryDetails = ({
entity, entity,
hasRefreshButton, hasRefreshButton,
hasAttachDetach,
fake, fake,
onRefresh, onRefresh,
}: ISummaryDetails) => { }: ISummaryDetails) => {
const cardContentRef = useRef(null); const cardContentRef = useRef<HTMLDivElement>(null);
const hasDetails = entity.details && entity.details.length > 0; const hasDetails = entity.details && entity.details.length > 0;
return ( return (
<> <>
<div <div className="flex items-center justify-between">
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<h2>{entity.name}</h2> <h2>{entity.name}</h2>
<div> <div>
{hasAttachDetach && (
<Button className="mr-6" variant="contained">
Attach / Detach
</Button>
)}
{hasRefreshButton && ( {hasRefreshButton && (
<Button onClick={onRefresh} variant="contained"> <Button onClick={onRefresh} variant="contained">
Refresh Refresh
@@ -46,7 +34,7 @@ const SummaryDetails = ({
{hasDetails && ( {hasDetails && (
<Card variant="outlined"> <Card variant="outlined">
<CardHeader <CardHeader
subheader={fake ? "Summary (Fake Data)" : "Summary"} subheader={`Summary ${fake ? "(Fake Data)" : ""}`}
action={<CopyToClipboard contentRef={cardContentRef} />} action={<CopyToClipboard contentRef={cardContentRef} />}
/> />
<CardContent ref={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 { Checkbox, Skeleton } from "@mui/material";
import ErrorBoundary from "@/components/error_boundary"; import ErrorBoundary from "@/components/error_boundary";
const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => { const CustomTable = ({
configuration,
data,
loading,
tkey,
onConsumeAction,
}: ICustomTable) => {
if (loading) if (loading)
return <Skeleton variant="rectangular" animation="wave" height={200} />; return <Skeleton variant="rectangular" animation="wave" height={200} />;
@@ -23,7 +29,12 @@ const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
const renderTableCell = ( const renderTableCell = (
value: any, value: any,
cellKey: string, cellKey: string,
render?: (param: any) => void | undefined, render?: (
param: any,
data?: any,
onFunc?: (param: any) => void,
) => void | undefined,
rowData?: any,
) => { ) => {
let renderedValue = value; let renderedValue = value;
@@ -35,10 +46,17 @@ const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
renderedValue = <Checkbox disabled checked={value} />; renderedValue = <Checkbox disabled checked={value} />;
// cover use case if we want to render a component // cover use case if we want to render a component
if (render) renderedValue = render(value); if (render) renderedValue = render(value, rowData, onConsumeAction);
if (typeof renderedValue === "object" && render === undefined) {
// 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); console.warn("Missing render function for column " + cellKey);
} }
return ( return (
<ErrorBoundary> <ErrorBoundary>
<StyledTableCell key={cellKey} align="left"> <StyledTableCell key={cellKey} align="left">
@@ -59,17 +77,18 @@ const CustomTable = ({ configuration, data, loading, tkey }: ICustomTable) => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{data.map((data: any, rowIndex: number) => ( {data.map((rowData: any, rowIndex: number) => (
<StyledTableRow key={rowIndex}> <StyledTableRow key={rowIndex}>
{configuration.map( {configuration.map(
(column: CustomTableConfiguration, columnIndex: number) => { (column: CustomTableConfiguration, columnIndex: number) => {
const cellValue: any = data[column.key]; const cellValue: any = rowData[column.key];
const cellKey = tkey + ":" + column.key + ":" + rowIndex; const cellKey = tkey + ":" + column.key + ":" + rowIndex;
const renderComponent = column?.render; const renderComponent = column?.render;
return renderTableCell( return renderTableCell(
cellValue, cellValue,
cellKey + ":" + columnIndex, cellKey + ":" + columnIndex,
renderComponent, 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 }) => ({ export const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
// backgroundColor: theme.palette.common.black,
backgroundColor: "#003258", backgroundColor: "#003258",
color: theme.palette.common.white, color: theme.palette.common.white,
}, },

View File

@@ -1,19 +1,4 @@
// AP - Summary // AP - 2 Tables Configurations to display labels
export const APSummaryDetails = [
{
label: "DID",
value: "did:sov:test:1274",
},
{
label: "IP",
value: "127.0.0.2",
},
{
label: "Network",
value: "Carlo's Home Network",
},
];
export const APAttachmentsTableConfig = [ export const APAttachmentsTableConfig = [
{ {
@@ -61,29 +46,9 @@ export const APServiceRepositoryTableConfig = [
label: "Status", label: "Status",
render: (value: any) => { render: (value: any) => {
let renderedValue: any = ""; let renderedValue: any = "";
if (Array.isArray(value.data)) { if (Array.isArray(value.data)) renderedValue = value.data.join(", ");
renderedValue = value.data.join(", "); else console.error("Status is not an array", value);
} else {
console.error("Status is not an array", value);
}
return renderedValue; 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 EntityActions from "@/components/entity_actions";
import AddCircleIcon from "@mui/icons-material/AddCircle"; import ConsumeAction from "@/components/consume_action";
import RemoveCircleIcon from "@mui/icons-material/RemoveCircle";
import DeleteIcon from "@mui/icons-material/Delete";
export const ClientTableConfig = [ export const ClientTableConfig = [
{ {
@@ -15,11 +13,13 @@ export const ClientTableConfig = [
{ {
key: "endpoint_url", key: "endpoint_url",
label: "End Point", label: "End Point",
render: () => { render: (value: any, rowData: any, onConsume: any) => {
return ( return (
<Button disabled variant="outlined"> <ConsumeAction
Consume rowData={rowData}
</Button> onConsume={onConsume}
endpoint={value}
/>
); );
}, },
}, },
@@ -88,39 +88,10 @@ export const ServiceTableConfig = [
{ {
key: "action", key: "action",
label: "Actions", label: "Actions",
render: () => { render: (value: any, rowData?: any) => {
return ( if (value && value?.data.length > 0)
<> return <EntityActions rowData={rowData} endpointData={value.data} />;
<Tooltip title="Register" placement="top"> else return "N/A";
<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;
}, },
}, },
]; ];

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"; import { formatDateTime } from "@/utils/helpers";
export const DLGSummaryDetails = [ // DLG - 2 Tables Configurations to display labels
{
label: "DID",
value: "did:sov:test:1274",
},
{
label: "URL",
value: "dlg.tu-berlin.de",
},
];
// DLG Resolution Table
export const DLGResolutionDummyData = [ 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 { export interface CustomTableConfiguration {
key: string; key: string;
label: string; label: string;
render?: (param: any) => void; render?: (param: any, rowData?: any, onConsume?: any) => void;
} }
export interface ICustomTable { export interface ICustomTable {
@@ -9,22 +9,27 @@ export interface ICustomTable {
data: any; data: any;
loading?: boolean; loading?: boolean;
tkey: string; tkey: string;
onConsumeAction?: (param: any) => void;
} }
export interface EntityDetails { export interface EntityDetails {
label: string; label: string;
value: string; value: string | undefined;
} }
export interface Entity { export interface Entity {
name: string; name?: string;
details: EntityDetails[]; details: EntityDetails[];
} }
export interface ISummaryDetails { export interface ISummaryDetails {
entity: any; entity: Entity;
fake?: boolean; fake?: boolean;
hasRefreshButton?: boolean; hasRefreshButton?: boolean;
hasAttachDetach?: boolean;
onRefresh?: () => void; onRefresh?: () => void;
} }
export interface IEntityActions {
name: string;
endpoint: string;
}

View File

@@ -1,5 +1,8 @@
export const formatDateTime = (date: string) => { import { projectConfig } from "@/config/config";
const _date = new Date(date);
export const formatDateTime = (date: string | number) => {
const dateToFormat = typeof date === "number" ? date * 1000 : date;
const _date = new Date(dateToFormat);
return _date.toLocaleDateString("en-US", { return _date.toLocaleDateString("en-US", {
year: "numeric", year: "numeric",
month: "long", month: "long",
@@ -10,3 +13,19 @@ export const formatDateTime = (date: string) => {
hour12: true, 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 : {};
}