From 7a9be0e18ae442b717ff38a3a230b183c5ba3b84 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Tue, 15 Oct 2024 14:45:11 +0800 Subject: [PATCH 01/28] feat: add profile api schema --- free5gc-Webconsole.postman_collection.json | 102 ++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/free5gc-Webconsole.postman_collection.json b/free5gc-Webconsole.postman_collection.json index 82febe9a..f8b6b46c 100644 --- a/free5gc-Webconsole.postman_collection.json +++ b/free5gc-Webconsole.postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "227e7242-725b-4caa-b1bd-4eec7dc4f424", + "_postman_id": "26b086b1-ac04-4a3d-b3ff-3154cc0acb98", "name": "free5gc-Webconsole", "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", - "_exporter_id": "27798737" + "_exporter_id": "38955132" }, "item": [ { @@ -1570,6 +1570,99 @@ "body": "[\n {\n \"chargingMethod\": \"Offline\",\n \"dnn\": \"\",\n \"filter\": \"\",\n \"quota\": \"0\",\n \"ratingGroup\": 5,\n \"servingPlmnId\": \"20893\",\n \"snssai\": \"01010203\",\n \"ueId\": \"imsi-208930000000001\",\n \"unitCost\": \"1\"\n },\n {\n \"chargingMethod\": \"Offline\",\n \"dnn\": \"internet\",\n \"filter\": \"9.9.9.9/32\",\n \"qosRef\": 1,\n \"quota\": \"0\",\n \"ratingGroup\": 6,\n \"servingPlmnId\": \"20893\",\n \"snssai\": \"01010203\",\n \"ueId\": \"imsi-208930000000001\",\n \"unitCost\": \"1\"\n },\n {\n \"chargingMethod\": \"Offline\",\n \"dnn\": \"\",\n \"filter\": \"\",\n \"quota\": \"0\",\n \"ratingGroup\": 1,\n \"servingPlmnId\": \"20893\",\n \"snssai\": \"01010203\",\n \"ueId\": \"imsi-208930000000002\",\n \"unitCost\": \"1\"\n },\n {\n \"chargingMethod\": \"Offline\",\n \"dnn\": \"internet\",\n \"filter\": \"1.1.1.1/32\",\n \"qosRef\": 1,\n \"quota\": \"0\",\n \"ratingGroup\": 2,\n \"servingPlmnId\": \"20893\",\n \"snssai\": \"01010203\",\n \"ueId\": \"imsi-208930000000002\",\n \"unitCost\": \"1\"\n }\n]" } ] + }, + { + "name": "Delete Profile", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": "http://{{WEB_URL}}/api/profile/{{PROFILE_NAME}}" + }, + "response": [] + }, + { + "name": "Add a Profile", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"profileName\": \"test\",\n \"AccessAndMobilitySubscriptionData\": {\n \"gpsis\": [\n \"msisdn-0900000000\"\n ],\n \"nssai\": {\n \"defaultSingleNssais\": [\n {\n \"sst\": 1,\n \"sd\": \"010203\",\n \"isDefault\": true\n },\n {\n \"sst\": 1,\n \"sd\": \"112233\",\n \"isDefault\": true\n }\n ],\n \"singleNssais\": []\n },\n \"subscribedUeAmbr\": {\n \"downlink\": \"2 Gbps\",\n \"uplink\": \"1 Gbps\"\n }\n },\n \"SessionManagementSubscriptionData\": [\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n },\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n }\n ],\n \"SmfSelectionSubscriptionData\": {\n \"subscribedSnssaiInfos\": {\n \"01010203\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n },\n \"01112233\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n }\n }\n },\n \"AmPolicyData\": {\n \"subscCats\": [\n \"free5gc\"\n ]\n },\n \"SmPolicyData\": {\n \"smPolicySnssaiData\": {\n \"01010203\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n },\n \"01112233\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n }\n }\n },\n \"FlowRules\": [\n {\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"filter\": \"permit out 17 from 192.168.0.0/24 8000 to 60.60.0.0/24 \",\n \"qfi\": 5\n }\n ],\n \"QosFlows\": [\n {\n \"qfi\": 5, \n \"5qi\": 5,\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"gbrUL\": \"\",\n \"gbrDL\": \"\",\n \"mbrUL\": \"200 Mbps\",\n \"mbrDL\": \"100 Mbps\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://{{WEB_URL}}/api/profile" + }, + "response": [] + }, + { + "name": "Modify a Profile", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"profileName\": \"test\",\n \"AccessAndMobilitySubscriptionData\": {\n \"gpsis\": [\n \"msisdn-0900000000\"\n ],\n \"nssai\": {\n \"defaultSingleNssais\": [\n {\n \"sst\": 1,\n \"sd\": \"010203\",\n \"isDefault\": true\n },\n {\n \"sst\": 1,\n \"sd\": \"112233\",\n \"isDefault\": true\n }\n ],\n \"singleNssais\": []\n },\n \"subscribedUeAmbr\": {\n \"downlink\": \"2 Gbps\",\n \"uplink\": \"1 Gbps\"\n }\n },\n \"SessionManagementSubscriptionData\": [\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n },\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n }\n ],\n \"SmfSelectionSubscriptionData\": {\n \"subscribedSnssaiInfos\": {\n \"01010203\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n },\n \"01112233\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n }\n }\n },\n \"AmPolicyData\": {\n \"subscCats\": [\n \"free5gc\"\n ]\n },\n \"SmPolicyData\": {\n \"smPolicySnssaiData\": {\n \"01010203\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n },\n \"01112233\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n }\n }\n },\n \"FlowRules\": [\n {\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"filter\": \"permit out 17 from 192.168.0.0/24 8000 to 60.60.0.0/24 \",\n \"qfi\": 5\n }, {\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"filter\": \"permit out 17 from 192.169.0.0/24 8000 to 60.60.0.0/24 \",\n \"qfi\": 1\n }\n ],\n \"QosFlows\": [\n {\n \"qfi\": 5, \n \"5qi\": 5,\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"gbrUL\": \"\",\n \"gbrDL\": \"\",\n \"mbrUL\": \"200 Mbps\",\n \"mbrDL\": \"100 Mbps\"\n }, {\n \"qfi\": 1, \n \"5qi\": 1,\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"gbrUL\": \"200 Mbps\",\n \"gbrDL\": \"100 Mbps\",\n \"mbrUL\": \"200 Mbps\",\n \"mbrDL\": \"100 Mbps\"\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "http://{{WEB_URL}}/api/profile/{{PROFILE_NAME}}" + }, + "response": [] + }, + { + "name": "Get all Profiles", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": "http://{WEB_URL}}/api/profile" + }, + "response": [] + }, + { + "name": "Get a Profile", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": "http://{{WEB_URL}}/api/profile/{{PROFILE_NAME}}" + }, + "response": [] } ], "event": [ @@ -1613,6 +1706,11 @@ { "key": "ChargingMethod", "value": "Offline" + }, + { + "key": "PROFILE_NAME", + "value": "test", + "type": "default" } ] } \ No newline at end of file From fb313a9bc0f25a0e6e4dedd46119bc9e8abfd214 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Tue, 15 Oct 2024 15:18:05 +0800 Subject: [PATCH 02/28] feat: add profile model --- backend/WebUI/model_profile.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/WebUI/model_profile.go diff --git a/backend/WebUI/model_profile.go b/backend/WebUI/model_profile.go new file mode 100644 index 00000000..d13b049f --- /dev/null +++ b/backend/WebUI/model_profile.go @@ -0,0 +1,15 @@ +package WebUI + +import "github.com/free5gc/openapi/models" + +type Profile struct { + ProfileName string `json:"profileName"` + AccessAndMobilitySubscriptionData models.AccessAndMobilitySubscriptionData `json:"AccessAndMobilitySubscriptionData"` + SessionManagementSubscriptionData []models.SessionManagementSubscriptionData `json:"SessionManagementSubscriptionData"` + SmfSelectionSubscriptionData models.SmfSelectionSubscriptionData `json:"SmfSelectionSubscriptionData"` + AmPolicyData models.AmPolicyData `json:"AmPolicyData"` + SmPolicyData models.SmPolicyData `json:"SmPolicyData"` + FlowRules []FlowRule `json:"FlowRules"` + QosFlows []QosFlow `json:"QosFlows"` + ChargingData []ChargingData `json:"ChargingData"` +} From 173a2ce5187d25757b050240336239661ba1a5b2 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Wed, 23 Oct 2024 08:43:13 +0000 Subject: [PATCH 03/28] fix: rewrite the fileter example in flowrules --- free5gc-Webconsole.postman_collection.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/free5gc-Webconsole.postman_collection.json b/free5gc-Webconsole.postman_collection.json index f8b6b46c..31176b71 100644 --- a/free5gc-Webconsole.postman_collection.json +++ b/free5gc-Webconsole.postman_collection.json @@ -1599,7 +1599,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"profileName\": \"test\",\n \"AccessAndMobilitySubscriptionData\": {\n \"gpsis\": [\n \"msisdn-0900000000\"\n ],\n \"nssai\": {\n \"defaultSingleNssais\": [\n {\n \"sst\": 1,\n \"sd\": \"010203\",\n \"isDefault\": true\n },\n {\n \"sst\": 1,\n \"sd\": \"112233\",\n \"isDefault\": true\n }\n ],\n \"singleNssais\": []\n },\n \"subscribedUeAmbr\": {\n \"downlink\": \"2 Gbps\",\n \"uplink\": \"1 Gbps\"\n }\n },\n \"SessionManagementSubscriptionData\": [\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n },\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n }\n ],\n \"SmfSelectionSubscriptionData\": {\n \"subscribedSnssaiInfos\": {\n \"01010203\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n },\n \"01112233\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n }\n }\n },\n \"AmPolicyData\": {\n \"subscCats\": [\n \"free5gc\"\n ]\n },\n \"SmPolicyData\": {\n \"smPolicySnssaiData\": {\n \"01010203\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n },\n \"01112233\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n }\n }\n },\n \"FlowRules\": [\n {\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"filter\": \"permit out 17 from 192.168.0.0/24 8000 to 60.60.0.0/24 \",\n \"qfi\": 5\n }\n ],\n \"QosFlows\": [\n {\n \"qfi\": 5, \n \"5qi\": 5,\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"gbrUL\": \"\",\n \"gbrDL\": \"\",\n \"mbrUL\": \"200 Mbps\",\n \"mbrDL\": \"100 Mbps\"\n }\n ]\n}", + "raw": "{\n \"profileName\": \"test\",\n \"AccessAndMobilitySubscriptionData\": {\n \"gpsis\": [\n \"msisdn-0900000000\"\n ],\n \"nssai\": {\n \"defaultSingleNssais\": [\n {\n \"sst\": 1,\n \"sd\": \"010203\",\n \"isDefault\": true\n },\n {\n \"sst\": 1,\n \"sd\": \"112233\",\n \"isDefault\": true\n }\n ],\n \"singleNssais\": []\n },\n \"subscribedUeAmbr\": {\n \"downlink\": \"2 Gbps\",\n \"uplink\": \"1 Gbps\"\n }\n },\n \"SessionManagementSubscriptionData\": [\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n },\n {\n \"singleNssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"dnnConfigurations\": {\n \"internet\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n },\n \"internet2\": {\n \"sscModes\": {\n \"defaultSscMode\": \"SSC_MODE_1\",\n \"allowedSscModes\": [\n \"SSC_MODE_2\",\n \"SSC_MODE_3\"\n ]\n },\n \"pduSessionTypes\": {\n \"defaultSessionType\": \"IPV4\",\n \"allowedSessionTypes\": [\n \"IPV4\"\n ]\n },\n \"sessionAmbr\": {\n \"uplink\": \"200 Mbps\",\n \"downlink\": \"100 Mbps\"\n },\n \"5gQosProfile\": {\n \"5qi\": 9,\n \"arp\": {\n \"priorityLevel\": 8\n },\n \"priorityLevel\": 8\n }\n }\n }\n }\n ],\n \"SmfSelectionSubscriptionData\": {\n \"subscribedSnssaiInfos\": {\n \"01010203\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n },\n \"01112233\": {\n \"dnnInfos\": [\n {\n \"dnn\": \"internet\"\n },\n {\n \"dnn\": \"internet2\"\n }\n ]\n }\n }\n },\n \"AmPolicyData\": {\n \"subscCats\": [\n \"free5gc\"\n ]\n },\n \"SmPolicyData\": {\n \"smPolicySnssaiData\": {\n \"01010203\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"010203\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n },\n \"01112233\": {\n \"snssai\": {\n \"sst\": 1,\n \"sd\": \"112233\"\n },\n \"smPolicyDnnData\": {\n \"internet\": {\n \"dnn\": \"internet\"\n },\n \"internet2\": {\n \"dnn\": \"internet2\"\n }\n }\n }\n }\n },\n \"FlowRules\": [\n {\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"filter\": \"1.1.1.1/32\",\n \"qfi\": 5\n }\n ],\n \"QosFlows\": [\n {\n \"qfi\": 5, \n \"5qi\": 5,\n \"snssai\": \"01010203\",\n \"dnn\": \"internet\",\n \"gbrUL\": \"\",\n \"gbrDL\": \"\",\n \"mbrUL\": \"200 Mbps\",\n \"mbrDL\": \"100 Mbps\"\n }\n ]\n}", "options": { "raw": { "language": "json" From 8685fcb314edc036a017745e8459127d51bd7611 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Thu, 24 Oct 2024 07:59:22 +0000 Subject: [PATCH 04/28] feat: add openapi generator docker script --- frontend/openapi-generator-docker.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 frontend/openapi-generator-docker.sh diff --git a/frontend/openapi-generator-docker.sh b/frontend/openapi-generator-docker.sh new file mode 100755 index 00000000..c5bea708 --- /dev/null +++ b/frontend/openapi-generator-docker.sh @@ -0,0 +1,11 @@ +#! /bin/bash + +# prerequisites +# - docker + +# use Docker to run OpenAPI Generator +docker run --rm -v $PWD:/local openapitools/openapi-generator-cli generate -i /local/webconsole.yaml -g typescript-axios -o /local/src/api + +# replace Time with Date in the file +sed 's/: Time/: Date/g' /local/src/api/api.ts > /local/src/api/api.ts.mod +mv /local/src/api/api.ts.mod /local/src/api/api.ts # rename the replaced file to the original file \ No newline at end of file From f4932d569dd0045fc31baca062a05d7b34e3e8e0 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Mon, 28 Oct 2024 13:39:47 +0000 Subject: [PATCH 05/28] feat: add backend profile api handler function and its corresponds routing method --- backend/WebUI/api_webui.go | 189 ++++++ ...model_profile.go => model_profile_data.go} | 4 +- backend/WebUI/model_profile_list_ie.go | 6 + backend/WebUI/routers.go | 35 ++ frontend/src/App.tsx | 36 +- frontend/src/ListItems.tsx | 9 + frontend/src/api/api.ts | 160 ++++- frontend/src/hooks/profile-form.tsx | 43 ++ frontend/src/lib/dtos/profile.ts | 552 ++++++++++++++++++ .../pages/ProfileCreate/FormCharingConfig.tsx | 201 +++++++ .../src/pages/ProfileCreate/FormFlowRule.tsx | 267 +++++++++ .../pages/ProfileCreate/FormUpSecurity.tsx | 161 +++++ .../pages/ProfileCreate/ProfileFormBasic.tsx | 35 ++ .../ProfileCreate/ProfileFormSessions.tsx | 375 ++++++++++++ .../pages/ProfileCreate/ProfileFormUeAmbr.tsx | 43 ++ frontend/src/pages/ProfileCreate/index.tsx | 148 +++++ frontend/src/pages/ProfileList.tsx | 116 ++++ frontend/src/pages/ProfileRead.tsx | 345 +++++++++++ frontend/webconsole.yaml | 76 +++ 19 files changed, 2797 insertions(+), 4 deletions(-) rename backend/WebUI/{model_profile.go => model_profile_data.go} (80%) create mode 100644 backend/WebUI/model_profile_list_ie.go create mode 100644 frontend/src/hooks/profile-form.tsx create mode 100644 frontend/src/lib/dtos/profile.ts create mode 100644 frontend/src/pages/ProfileCreate/FormCharingConfig.tsx create mode 100644 frontend/src/pages/ProfileCreate/FormFlowRule.tsx create mode 100644 frontend/src/pages/ProfileCreate/FormUpSecurity.tsx create mode 100644 frontend/src/pages/ProfileCreate/ProfileFormBasic.tsx create mode 100644 frontend/src/pages/ProfileCreate/ProfileFormSessions.tsx create mode 100644 frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx create mode 100644 frontend/src/pages/ProfileCreate/index.tsx create mode 100644 frontend/src/pages/ProfileList.tsx create mode 100644 frontend/src/pages/ProfileRead.tsx diff --git a/backend/WebUI/api_webui.go b/backend/WebUI/api_webui.go index 9f8f7c16..aeacdd05 100644 --- a/backend/WebUI/api_webui.go +++ b/backend/WebUI/api_webui.go @@ -40,6 +40,8 @@ const ( userDataColl = "userData" tenantDataColl = "tenantData" identityDataColl = "subscriptionData.identityData" + profileListColl = "profileList" // store profile name and gpsi + profileDataColl = "profileData" // store profile data ) var jwtKey = "" // for generating JWT @@ -1952,3 +1954,190 @@ func OptionsSubscribers(c *gin.Context) { c.JSON(http.StatusNoContent, gin.H{}) } + +// Delete profile by profileName +func DeleteProfile(c *gin.Context) { + setCorsHeader(c) + logger.ProcLog.Infoln("Delete One Profile Data") + + profileName := c.Param("profileName") + pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profileName}) + if err != nil { + logger.ProcLog.Errorf("DeleteProfile err: %+v", err) + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + if len(pf) == 0 { + c.JSON(http.StatusNotFound, gin.H{ + "cause": "Profile does not exist", + }) + return + } + + dbProfileOperation(profileName, "delete", nil) + c.JSON(http.StatusNoContent, gin.H{}) +} + +// Get profile list +func GetProfiles(c *gin.Context) { + setCorsHeader(c) + logger.ProcLog.Infoln("Get All Profiles List") + + _, err := GetTenantId(c) + if err != nil { + logger.ProcLog.Errorln(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{ + "cause": "Illegal Token", + }) + return + } + + var pfList []ProfileListIE = make([]ProfileListIE, 0) + profileList, err := mongoapi.RestfulAPIGetMany(profileListColl, bson.M{}) + if err != nil { + logger.ProcLog.Errorf("GetProfiles err: %+v", err) + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + for _, profile := range profileList { + profileName := profile["profileName"] + gpsi := profile["gpsi"] + + tmp := ProfileListIE{ + ProfileName: profileName.(string), + Gpsi: gpsi.(string), + } + pfList = append(pfList, tmp) + } + c.JSON(http.StatusOK, pfList) +} + +// Get profile by profileName +func GetProfile(c *gin.Context) { + setCorsHeader(c) + logger.ProcLog.Infoln("Get One Profile Data") + + profileName := c.Param("profileName") + + profile, err := mongoapi.RestfulAPIGetOne(profileDataColl, bson.M{"profileName": profileName}) + if err != nil { + logger.ProcLog.Errorf("GetProfile err: %+v", err) + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + var pf Profile + err = json.Unmarshal(mapToByte(profile), &pf) + if err != nil { + logger.ProcLog.Errorf("JSON Unmarshal err: %+v", err) + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + c.JSON(http.StatusOK, pf) +} + +// Post profile +func PostProfile(c *gin.Context) { + setCorsHeader(c) + logger.ProcLog.Infoln("Post One Profile Data") + + tokenStr := c.GetHeader("Token") + _, err := ParseJWT(tokenStr) + if err != nil { + logger.ProcLog.Errorln(err.Error()) + c.JSON(http.StatusBadRequest, gin.H{ + "cause": "Illegal Token", + }) + return + } + + var profile Profile + if err := c.ShouldBindJSON(&profile); err != nil { + logger.ProcLog.Errorf("PostProfile err: %+v", err) + c.JSON(http.StatusBadRequest, gin.H{ + "cause": "JSON format incorrect", + }) + return + } + + pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profile.ProfileName}) + if err != nil { + logger.ProcLog.Errorf("GetProfile err: %+v", err) + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + if len(pf) != 0 { + c.JSON(http.StatusConflict, gin.H{ + "cause": "Profile already exists", + }) + return + } + + logger.ProcLog.Infof("PostProfile: %+v", profile.ProfileName) + dbProfileOperation(profile.ProfileName, "post", &profile) + c.JSON(http.StatusCreated, gin.H{}) +} + +// Put profile by profileName +func PutProfile(c *gin.Context) { + setCorsHeader(c) + logger.ProcLog.Infoln("Put One Profile Data") + + profileName := c.Param("profileName") + + var profile Profile + if err := c.ShouldBindJSON(&profile); err != nil { + logger.ProcLog.Errorf("PutProfile err: %+v", err) + c.JSON(http.StatusBadRequest, gin.H{}) + return + } + + pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profile.ProfileName}) + if err != nil { + logger.ProcLog.Errorf("PutProfile err: %+v", err) + c.JSON(http.StatusInternalServerError, gin.H{}) + return + } + if len(pf) == 0 { + c.JSON(http.StatusNotFound, gin.H{ + "cause": "Profile does not exist", + }) + return + } + + logger.ProcLog.Infof("PutProfile: %+v", profile.ProfileName) + dbProfileOperation(profileName, "put", &profile) + c.JSON(http.StatusNoContent, gin.H{}) +} + + +func dbProfileOperation(profileName string, method string, profile *Profile) { + filter := bson.M{"profileName": profileName} + + // Replace all data with new one + if method == "put" { + if err := mongoapi.RestfulAPIDeleteOne(profileDataColl, filter); err != nil { + logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) + } + } else if method == "delete" { + if err := mongoapi.RestfulAPIDeleteOne(profileListColl, filter); err != nil { + logger.ProcLog.Errorf("DeleteSubscriberByID err: %+v", err) + } + if err := mongoapi.RestfulAPIDeleteOne(profileDataColl, filter); err != nil { + logger.ProcLog.Errorf("DeleteSubscriberByID err: %+v", err) + } + } + if method == "post" || method == "put" { + profileListIE := ProfileListIE{ + ProfileName: profileName, + Gpsi: "", + } + profileListIEBsonM := toBsonM(profileListIE) + profileBsonM := toBsonM(profile) + if _, err := mongoapi.RestfulAPIPost(profileListColl, filter, profileListIEBsonM); err != nil { + logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) + } + if _, err := mongoapi.RestfulAPIPost(profileDataColl, filter, profileBsonM); err != nil { + logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) + } + } +} \ No newline at end of file diff --git a/backend/WebUI/model_profile.go b/backend/WebUI/model_profile_data.go similarity index 80% rename from backend/WebUI/model_profile.go rename to backend/WebUI/model_profile_data.go index d13b049f..525b6c77 100644 --- a/backend/WebUI/model_profile.go +++ b/backend/WebUI/model_profile_data.go @@ -3,7 +3,7 @@ package WebUI import "github.com/free5gc/openapi/models" type Profile struct { - ProfileName string `json:"profileName"` + ProfileName string `json:"profileName"` AccessAndMobilitySubscriptionData models.AccessAndMobilitySubscriptionData `json:"AccessAndMobilitySubscriptionData"` SessionManagementSubscriptionData []models.SessionManagementSubscriptionData `json:"SessionManagementSubscriptionData"` SmfSelectionSubscriptionData models.SmfSelectionSubscriptionData `json:"SmfSelectionSubscriptionData"` @@ -11,5 +11,5 @@ type Profile struct { SmPolicyData models.SmPolicyData `json:"SmPolicyData"` FlowRules []FlowRule `json:"FlowRules"` QosFlows []QosFlow `json:"QosFlows"` - ChargingData []ChargingData `json:"ChargingData"` + ChargingDatas []ChargingData } diff --git a/backend/WebUI/model_profile_list_ie.go b/backend/WebUI/model_profile_list_ie.go new file mode 100644 index 00000000..673053b7 --- /dev/null +++ b/backend/WebUI/model_profile_list_ie.go @@ -0,0 +1,6 @@ +package WebUI + +type ProfileListIE struct { + ProfileName string `json:"profileName"` + Gpsi string `json:"gpsi"` +} diff --git a/backend/WebUI/routers.go b/backend/WebUI/routers.go index d513b10c..bf3c25da 100644 --- a/backend/WebUI/routers.go +++ b/backend/WebUI/routers.go @@ -250,4 +250,39 @@ var routes = Routes{ "/verify-staticip", VerifyStaticIP, }, + + { + "Delete Profile", + http.MethodDelete, + "/profile/:profileName", + DeleteProfile, + }, + + { + "Get Profile List", + http.MethodGet, + "/profile", + GetProfiles, + }, + + { + "Get Profile", + http.MethodGet, + "/profile/:profileName", + GetProfile, + }, + + { + "Post Profile", + http.MethodPost, + "/profile", + PostProfile, + }, + + { + "Put Profile", + http.MethodPut, + "/profile/:profileName", + PutProfile, + }, } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 011f934d..0187aebe 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,7 +17,9 @@ import ChangePassword from "./pages/ChangePassword"; import ChargingTable from "./pages/Charging/ChargingTable"; import { ProtectedRoute } from "./ProtectedRoute"; import { LoginContext, User } from "./LoginContext"; - +import ProfileList from "./pages/ProfileList"; +import ProfileCreate from "./pages/ProfileCreate"; +import ProfileRead from "./pages/ProfileRead"; export default function App() { const [user, setUser] = useState(() => { // retrieve from local storage on initial load (if available) @@ -182,6 +184,38 @@ export default function App() { } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> diff --git a/frontend/src/ListItems.tsx b/frontend/src/ListItems.tsx index 2a91cfec..4df1b3cb 100644 --- a/frontend/src/ListItems.tsx +++ b/frontend/src/ListItems.tsx @@ -7,6 +7,7 @@ import PhoneAndroid from "@mui/icons-material/PhoneAndroid"; import FontDownload from "@mui/icons-material/FontDownload"; import SupervisorAccountOutlinedIcon from "@mui/icons-material/SupervisorAccountOutlined"; import AttachMoneyOutlinedIcon from "@mui/icons-material/AttachMoneyOutlined"; +import PersonIcon from "@mui/icons-material/Person"; import { Link } from "react-router-dom"; import { LoginContext } from "./LoginContext"; @@ -43,6 +44,14 @@ export const MainListItems = () => { + + + + + + + + diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index fb4e6d16..136dd396 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -699,6 +699,86 @@ export interface PermanentKey { */ 'encryptionAlgorithm': number; } +/** + * + * @export + * @interface Profile + */ +export interface Profile { + /** + * + * @type {string} + * @memberof Profile + */ + 'profileName': string; + /** + * + * @type {AccessAndMobilitySubscriptionData} + * @memberof Profile + */ + 'AccessAndMobilitySubscriptionData': AccessAndMobilitySubscriptionData; + /** + * + * @type {Array} + * @memberof Profile + */ + 'SessionManagementSubscriptionData': Array; + /** + * + * @type {SmfSelectionSubscriptionData} + * @memberof Profile + */ + 'SmfSelectionSubscriptionData': SmfSelectionSubscriptionData; + /** + * + * @type {AmPolicyData} + * @memberof Profile + */ + 'AmPolicyData': AmPolicyData; + /** + * + * @type {SmPolicyData} + * @memberof Profile + */ + 'SmPolicyData': SmPolicyData; + /** + * + * @type {Array} + * @memberof Profile + */ + 'FlowRules': Array; + /** + * + * @type {Array} + * @memberof Profile + */ + 'QosFlows': Array; + /** + * + * @type {Array} + * @memberof Profile + */ + 'ChargingDatas': Array; +} +/** + * + * @export + * @interface ProfileListIE + */ +export interface ProfileListIE { + /** + * + * @type {string} + * @memberof ProfileListIE + */ + 'profileName'?: string; + /** + * + * @type {string} + * @memberof ProfileListIE + */ + 'gpsi'?: string; +} /** * * @export @@ -1161,6 +1241,46 @@ export interface User { */ export const WebconsoleApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * Returns an array of profile. + * @summary Get all profiles + * @param {number} [limit] + * @param {number} [page] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiProfileGet: async (limit?: number, page?: number, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/profile`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Returns an array of subscriber. * @summary Get all subscribers @@ -1211,6 +1331,20 @@ export const WebconsoleApiAxiosParamCreator = function (configuration?: Configur export const WebconsoleApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = WebconsoleApiAxiosParamCreator(configuration) return { + /** + * Returns an array of profile. + * @summary Get all profiles + * @param {number} [limit] + * @param {number} [page] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async apiProfileGet(limit?: number, page?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.apiProfileGet(limit, page, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['WebconsoleApi.apiProfileGet']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Returns an array of subscriber. * @summary Get all subscribers @@ -1235,6 +1369,17 @@ export const WebconsoleApiFp = function(configuration?: Configuration) { export const WebconsoleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = WebconsoleApiFp(configuration) return { + /** + * Returns an array of profile. + * @summary Get all profiles + * @param {number} [limit] + * @param {number} [page] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + apiProfileGet(limit?: number, page?: number, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.apiProfileGet(limit, page, options).then((request) => request(axios, basePath)); + }, /** * Returns an array of subscriber. * @summary Get all subscribers @@ -1243,7 +1388,7 @@ export const WebconsoleApiFactory = function (configuration?: Configuration, bas * @param {*} [options] Override http request option. * @throws {RequiredError} */ - apiSubscriberGet(limit?: number, page?: number, options?: any): AxiosPromise> { + apiSubscriberGet(limit?: number, page?: number, options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.apiSubscriberGet(limit, page, options).then((request) => request(axios, basePath)); }, }; @@ -1256,6 +1401,19 @@ export const WebconsoleApiFactory = function (configuration?: Configuration, bas * @extends {BaseAPI} */ export class WebconsoleApi extends BaseAPI { + /** + * Returns an array of profile. + * @summary Get all profiles + * @param {number} [limit] + * @param {number} [page] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WebconsoleApi + */ + public apiProfileGet(limit?: number, page?: number, options?: RawAxiosRequestConfig) { + return WebconsoleApiFp(this.configuration).apiProfileGet(limit, page, options).then((request) => request(this.axios, this.basePath)); + } + /** * Returns an array of subscriber. * @summary Get all subscribers diff --git a/frontend/src/hooks/profile-form.tsx b/frontend/src/hooks/profile-form.tsx new file mode 100644 index 00000000..68ae1ff0 --- /dev/null +++ b/frontend/src/hooks/profile-form.tsx @@ -0,0 +1,43 @@ +import { ReactNode } from "react"; +import { Profile } from "../api"; +import { FormProvider, UseFormProps, useForm, useFormContext } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { defaultProfileDTO, profileDTOSchema, type ProfileDTO } from "../lib/dtos/profile"; + +const ProfileFormOptions = { + mode: "onBlur", + reValidateMode: "onChange", + resolver: zodResolver(profileDTOSchema), + defaultValues: defaultProfileDTO(), +} satisfies UseFormProps; + +export const ProfileFormProvider = ({ children }: { children: ReactNode }) => { + const method = useForm(ProfileFormOptions); + return {children}; +}; + +export const useProfileForm = () => { + const { + register, + handleSubmit, + watch, + getValues, + setValue, + setFocus, + reset, + control, + formState: { errors: validationErrors }, + } = useFormContext(); + + return { + register, + validationErrors, + handleSubmit, + watch, + getValues, + control, + setValue, + setFocus, + reset, + }; +}; diff --git a/frontend/src/lib/dtos/profile.ts b/frontend/src/lib/dtos/profile.ts new file mode 100644 index 00000000..a7699d66 --- /dev/null +++ b/frontend/src/lib/dtos/profile.ts @@ -0,0 +1,552 @@ +import { z } from "zod"; +import { + FlowRules, + QosFlows, + ChargingData, + SubscribedUeAmbr, + Nssai, + SessionManagementSubscriptionData, + DnnConfiguration, + UpSecurity, + Profile, +} from "../../api" +import { DEFAULT_5QI } from "../const"; + +interface AmbrDTO { + uplink: string; + downlink: string; +} + +export const ambrDTOSchema = z.object({ + uplink: z.string(), + downlink: z.string(), +}) + +interface ChargingDataDTO { + chargingMethod: "Online" | "Offline"; + quota: string; + unitCost: string; +} + +export const chargingDataDTOSchema = z.object({ + chargingMethod: z.enum(["Online", "Offline"]), + quota: z.string(), + unitCost: z.string(), +}) + +interface FlowRulesDTO { + filter: string; + precedence: number; + "5qi": number; + gbrUL: string; + gbrDL: string; + mbrUL: string; + mbrDL: string; + chargingData: ChargingDataDTO; +} + +export const flowRulesDTOSchema = z.object({ + filter: z.string(), + precedence: z.number(), + "5qi": z.number(), + gbrUL: z.string(), + gbrDL: z.string(), + mbrUL: z.string(), + mbrDL: z.string(), + chargingData: chargingDataDTOSchema, +}) + +interface UpSecurityDTO { + upIntegr: string; + upConfid: string; +} + +export const upSecurityDTOSchema = z.object({ + upIntegr: z.string(), + upConfid: z.string(), +}) + +interface DnnConfigurationDTO { + default5qi: number; + sessionAmbr: AmbrDTO; + enableStaticIpv4Address: boolean; + staticIpv4Address?: string; + flowRules: FlowRulesDTO[]; + upSecurity?: UpSecurityDTO; +} + +export const dnnConfigurationDTOSchema = z.object({ + default5qi: z.number(), + sessionAmbr: ambrDTOSchema, + enableStaticIpv4Address: z.boolean(), + staticIpv4Address: z.string().optional(), + flowRules: z.array(flowRulesDTOSchema), + upSecurity: upSecurityDTOSchema.optional(), +}) + +interface SnssaiConfigurationDTO { + sst: number; + sd: string; + isDefault: boolean; + chargingData: ChargingDataDTO; + dnnConfigurations: { [key: string]: DnnConfigurationDTO }; +} + +export const snssaiConfigurationDTOSchema = z.object({ + sst: z.number(), + sd: z.string(), + isDefault: z.boolean(), + chargingData: chargingDataDTOSchema, + dnnConfigurations: z.record(dnnConfigurationDTOSchema), +}) + +interface ProfileDTO { + profileName: string; + gpsi?: string; + subscribedUeAmbr: AmbrDTO; + SnssaiConfigurations: SnssaiConfigurationDTO[]; +} + +export const profileDTOSchema = z.object({ + profileName: z.string(), + gpsi: z.string().optional(), + subscribedUeAmbr: ambrDTOSchema, + SnssaiConfigurations: z.array(snssaiConfigurationDTOSchema), +}) + +interface FlowsDTO { + flowRules: FlowRules[]; + qosFlows: QosFlows[]; + chargingDatas: ChargingData[]; +} + +interface FlowsMapper { + map(profile: ProfileDTO): FlowsDTO; +} + +class FlowsMapperImpl implements FlowsMapper { + refNumber: number = 1; + flowRules: FlowRules[] = []; + qosFlows: QosFlows[] = []; + chargingDatas: ChargingData[] = []; + + private buildDnns(profile: ProfileDTO): { + snssai: string; + dnn: string; + sliceCharingData: ChargingDataDTO; + flowRules: FlowRulesDTO[]; + }[] { + return profile.SnssaiConfigurations.reduce( + (acc, s) => { + const snssai = s.sst.toString().padStart(2, "0") + s.sd; + const dnns = Object.entries(s.dnnConfigurations).map(([dnn, dnnConfig]) => ({ + snssai: snssai, + dnn: dnn, + sliceCharingData: s.chargingData, + flowRules: dnnConfig.flowRules, + })); + dnns.forEach((dnn) => acc.push(dnn)); + return acc; + }, + [] as { + snssai: string; + dnn: string; + sliceCharingData: ChargingDataDTO; + flowRules: FlowRulesDTO[]; + }[], + ); + } + + map(profile: ProfileDTO): FlowsDTO { + const dnns = this.buildDnns(profile); + + dnns.forEach((dnn) => { + const snssai = dnn.snssai; + + this.chargingDatas.push({ + ...dnn.sliceCharingData, + snssai: snssai, + dnn: dnn.dnn, + filter: "", + }); + + dnn.flowRules.forEach((flow) => { + const qosRef = this.refNumber++; + + this.flowRules.push({ + filter: flow.filter, + precedence: flow.precedence, + snssai: snssai, + dnn: dnn.dnn, + qosRef, + }); + + this.qosFlows.push({ + snssai: snssai, + dnn: dnn.dnn, + qosRef, + "5qi": flow["5qi"], + mbrUL: flow.mbrUL, + mbrDL: flow.mbrDL, + gbrUL: flow.gbrUL, + gbrDL: flow.gbrDL, + }); + + this.chargingDatas.push({ + ...flow.chargingData, + snssai: snssai, + dnn: dnn.dnn, + filter: flow.filter, + }); + }); + }); + + return { + flowRules: this.flowRules, + qosFlows: this.qosFlows, + chargingDatas: this.chargingDatas, + }; + } +} + +interface ProfileMapper { + mapFromDto(profile: ProfileDTO): Profile; + mapFromProfile(profile: Profile): ProfileDTO; +} + +class ProfileMapperImpl implements ProfileMapper { + constructor(private readonly flowsBuilder: FlowsMapper) {} + + mapFromDto(profile: ProfileDTO): Profile { + const flows = this.flowsBuilder.map(profile); + + return { + profileName: profile.profileName, + AccessAndMobilitySubscriptionData: { + gpsis: [`msisdn-${profile.gpsi ?? ""}`], + subscribedUeAmbr: this.buildSubscriberAmbr(profile.subscribedUeAmbr), + nssai: { + defaultSingleNssais: profile.SnssaiConfigurations.filter((s) => s.isDefault).map( + (s) => this.buildNssai(s)), + singleNssais: profile.SnssaiConfigurations.filter((s) => !s.isDefault).map( + (s) => this.buildNssai(s)), + } + }, + SessionManagementSubscriptionData: profile.SnssaiConfigurations.map((s) => + this.buildSessionManagementSubscriptionData(s), + ), + + SmfSelectionSubscriptionData: { + subscribedSnssaiInfos: Object.fromEntries( + profile.SnssaiConfigurations.map((s) => [ + s.sst.toString().padStart(2, "0") + s.sd, + { + dnnInfos: Object.keys(s.dnnConfigurations).map((dnn) => ({ + dnn: dnn, + })), + }, + ]), + ), + }, + AmPolicyData: { + subscCats: ["free5gc"], + }, + SmPolicyData: { + smPolicySnssaiData: Object.fromEntries( + profile.SnssaiConfigurations.map((s) => [ + s.sst.toString().padStart(2, "0") + s.sd, + { + snssai: this.buildNssai(s), + smPolicyDnnData: Object.fromEntries( + Object.keys(s.dnnConfigurations).map((dnn) => [dnn, { dnn: dnn }]), + ), + }, + ]), + ), + }, + FlowRules: flows.flowRules, + QosFlows: flows.qosFlows, + ChargingDatas: flows.chargingDatas, + } + } + + mapFromProfile(profile: Profile): ProfileDTO { + return { + profileName: profile.profileName, + gpsi: profile.AccessAndMobilitySubscriptionData.gpsis?.[0]?.slice(7), + subscribedUeAmbr: { + uplink: profile.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.uplink ?? "", + downlink: profile.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.downlink ?? "", + }, + SnssaiConfigurations: profile.SessionManagementSubscriptionData.map((s) => ({ + sst: s.singleNssai.sst, + sd: s.singleNssai.sd ?? "", + isDefault: this.snssaiIsDefault(s.singleNssai, profile), + chargingData: this.findSliceChargingData(s.singleNssai, profile), + dnnConfigurations: Object.fromEntries( + Object.entries(s.dnnConfigurations ?? {}).map(([key, value]) => [ + key, + { + default5qi: value["5gQosProfile"]?.["5qi"] ?? DEFAULT_5QI, + sessionAmbr: { + uplink: value.sessionAmbr?.uplink ?? "", + downlink: value.sessionAmbr?.downlink ?? "", + }, + enableStaticIpv4Address: value.staticIpAddress?.length !== 0, + flowRules: this.parseDnnFlowRules(s.singleNssai, key, profile), + upSecurity: value.upSecurity, + } satisfies DnnConfigurationDTO, + ]), + ), + })), + } + } + + private snssaiIsDefault(nssai: Nssai, profile: Profile): boolean { + return ( + profile.AccessAndMobilitySubscriptionData.nssai?.defaultSingleNssais?.some( + (n) => n.sst === nssai.sst && n.sd === nssai.sd, + ) ?? false + ); + } + + private findSliceChargingData(nssai: Nssai, profile: Profile): ChargingDataDTO { + const chargingData = profile.ChargingDatas.find((c) => { + if (c.dnn !== "" || c.filter !== "") { + return false; + } + + return c.snssai === nssai.sst.toString().padStart(2, "0") + nssai.sd; + }); + + return { + chargingMethod: chargingData?.chargingMethod === "Online" ? "Online" : "Offline", + quota: chargingData?.quota ?? "", + unitCost: chargingData?.unitCost ?? "", + }; + } + + private parseDnnFlowRules(snssai: Nssai, dnn: string, profile: Profile): FlowRulesDTO[] { + const qosFlows = profile.QosFlows.filter( + (f) => f.dnn === dnn && f.snssai === snssai.sst.toString().padStart(2, "0") + snssai.sd, + ); + + return qosFlows.map((f) => { + const flowRule = profile.FlowRules.find((r) => r.qosRef === f.qosRef); + const chargingData = profile.ChargingDatas.find((c) => c.qosRef === f.qosRef); + + return { + filter: flowRule?.filter ?? "", + precedence: flowRule?.precedence ?? 0, + "5qi": f["5qi"] ?? DEFAULT_5QI, + gbrUL: f.gbrUL ?? "", + gbrDL: f.gbrDL ?? "", + mbrUL: f.mbrUL ?? "", + mbrDL: f.mbrDL ?? "", + chargingData: { + chargingMethod: chargingData?.chargingMethod === "Online" ? "Online" : "Offline", + quota: chargingData?.quota ?? "", + unitCost: chargingData?.unitCost ?? "", + }, + } + }) + } + + buildSubscriberAmbr(data: AmbrDTO): SubscribedUeAmbr{ + return { + uplink: data.uplink, + downlink: data.downlink, + } + } + + buildNssai(data: SnssaiConfigurationDTO): Nssai { + return { + sst: data.sst, + sd: data.sd, + } + } + + buildSessionManagementSubscriptionData(data: SnssaiConfigurationDTO): SessionManagementSubscriptionData { + return { + singleNssai: this.buildNssai(data), + dnnConfigurations: Object.fromEntries( + Object.entries(data.dnnConfigurations).map(([key, value]) => [ + key, + this.buildDnnConfiguration(value), + ]), + ), + } + } + + buildDnnConfiguration(data: DnnConfigurationDTO): DnnConfiguration { + return { + pduSessionTypes: { + defaultSessionType: "IPV4", + allowedSessionTypes: ["IPV4"], + }, + sscModes: { + defaultSscMode: "SSC_MODE_1", + allowedSscModes: ["SSC_MODE_2", "SSC_MODE_3"], + }, + "5gQosProfile": { + "5qi": data.default5qi, + arp: { + priorityLevel: 8, + preemptCap: "", + preemptVuln: "", + }, + priorityLevel: 8, + }, + sessionAmbr: this.buildSubscriberAmbr(data.sessionAmbr), + staticIpAddress: data.enableStaticIpv4Address ? [{ ipv4Addr: data.staticIpv4Address }] : [], + upSecurity: this.buildUpSecurity(data.upSecurity), + } + } + + buildUpSecurity(data: UpSecurityDTO | undefined): UpSecurity | undefined { + if (!data) { + return undefined; + } + + return { + upIntegr: data.upIntegr, + upConfid: data.upConfid, + } + } +} + +export const defaultProfileDTO = (): ProfileDTO => ({ + profileName: "profile-1", + gpsi: "", + subscribedUeAmbr: { + uplink: "1 Gbps", + downlink: "2 Gbps", + }, + SnssaiConfigurations: [ + { + sst: 1, + sd: "010203", + isDefault: true, + chargingData: { + chargingMethod: "Offline", + quota: "100000", + unitCost: "1", + }, + dnnConfigurations: { + internet: { + enableStaticIpv4Address: false, + default5qi: 9, + sessionAmbr: { + uplink: "1000 Mbps", + downlink: "1000 Mbps", + }, + flowRules: [ + { + filter: "1.1.1.1/32", + precedence: 128, + "5qi": 8, + gbrUL: "108 Mbps", + gbrDL: "108 Mbps", + mbrUL: "208 Mbps", + mbrDL: "208 Mbps", + chargingData: { + chargingMethod: "Offline", + quota: "100000", + unitCost: "1", + }, + }, + ], + }, + }, + }, + { + sst: 1, + sd: "112233", + isDefault: false, + chargingData: { + chargingMethod: "Online", + quota: "100000", + unitCost: "1", + }, + dnnConfigurations: { + internet: { + enableStaticIpv4Address: false, + default5qi: 8, + sessionAmbr: { + uplink: "1000 Mbps", + downlink: "1000 Mbps", + }, + flowRules: [ + { + filter: "1.1.1.1/32", + precedence: 127, + "5qi": 7, + gbrUL: "207 Mbps", + gbrDL: "207 Mbps", + mbrUL: "407 Mbps", + mbrDL: "407 Mbps", + chargingData: { + chargingMethod: "Online", + quota: "5000", + unitCost: "1", + }, + }, + ], + }, + }, + }, + ], +}) + +export const defaultSnssaiConfiguration = (): SnssaiConfigurationDTO => ({ + sst: 1, + sd: "", + isDefault: false, + chargingData: { + chargingMethod: "Offline", + quota: "100000", + unitCost: "1", + }, + dnnConfigurations: { internet: defaultDnnConfig() }, +}); + +export const defaultDnnConfig = (): DnnConfigurationDTO => ({ + default5qi: DEFAULT_5QI, + sessionAmbr: { + uplink: "1000 Mbps", + downlink: "1000 Mbps", + }, + enableStaticIpv4Address: false, + staticIpv4Address: "", + flowRules: [defaultFlowRule()], + upSecurity: undefined, +}); + +export const defaultFlowRule = (): FlowRulesDTO => ({ + filter: "1.1.1.1/32", + precedence: 128, + "5qi": 9, + gbrUL: "208 Mbps", + gbrDL: "208 Mbps", + mbrUL: "108 Mbps", + mbrDL: "108 Mbps", + chargingData: { + chargingMethod: "Online", + quota: "10000", + unitCost: "1", + }, +}); + +export const defaultUpSecurity = (): UpSecurityDTO => ({ + upIntegr: "NOT_NEEDED", + upConfid: "NOT_NEEDED", +}); + +export { + type ProfileDTO, + type FlowsDTO, + type SnssaiConfigurationDTO, + type DnnConfigurationDTO, + ProfileMapperImpl, + FlowsMapperImpl, +} diff --git a/frontend/src/pages/ProfileCreate/FormCharingConfig.tsx b/frontend/src/pages/ProfileCreate/FormCharingConfig.tsx new file mode 100644 index 00000000..a9ef19af --- /dev/null +++ b/frontend/src/pages/ProfileCreate/FormCharingConfig.tsx @@ -0,0 +1,201 @@ +import { + FormControl, + InputLabel, + MenuItem, + Select, + Table, + TableBody, + TableCell, + TableRow, + TextField, +} from "@mui/material"; +import { useProfileForm } from "../../hooks/profile-form"; +import { Controller } from "react-hook-form"; + +interface FormCharginConfigProps { + snssaiIndex: number; + dnn?: string; + filterIndex?: number; +} + +function FormSliceChargingConfig({ snssaiIndex }: FormCharginConfigProps) { + const { register, validationErrors, watch, control } = useProfileForm(); + + const isOnlineCharging = + watch(`SnssaiConfigurations.${snssaiIndex}.chargingData.chargingMethod`) === "Online"; + + return ( + + + + + + Charging Method + ( + + )} + /> + + + + + + + + + + + + +
+ ); +} + +function FormFlowChargingConfig({ snssaiIndex, dnn, filterIndex }: FormCharginConfigProps) { + const { register, validationErrors, watch, control } = useProfileForm(); + + if (dnn === undefined) { + throw new Error("dnn is undefined"); + } + if (filterIndex === undefined) { + throw new Error("filterIndex is undefined"); + } + + const isOnlineCharging = + watch( + `SnssaiConfigurations.${snssaiIndex}.dnnConfigurations.${dnn}.flowRules.${filterIndex}.chargingData.chargingMethod`, + ) === "Online"; + + return ( + + + + + + Charging Method + ( + + )} + /> + + + + + + + + + + +
+ ); +} + +export default function FormChargingConfig(props: FormCharginConfigProps) { + if (props.dnn === undefined) { + return ; + } + + return ; +} diff --git a/frontend/src/pages/ProfileCreate/FormFlowRule.tsx b/frontend/src/pages/ProfileCreate/FormFlowRule.tsx new file mode 100644 index 00000000..35ccc2aa --- /dev/null +++ b/frontend/src/pages/ProfileCreate/FormFlowRule.tsx @@ -0,0 +1,267 @@ +import { + Button, + Box, + Card, + Grid, + Table, + TableBody, + TableCell, + TableRow, + TextField, +} from "@mui/material"; +import type { Nssai } from "../../api"; +import { useProfileForm } from "../../hooks/profile-form"; +import { toHex } from "../../lib/utils"; +import FormChargingConfig from "./FormCharingConfig"; +import { useFieldArray } from "react-hook-form"; +import { defaultFlowRule } from "../../lib/dtos/profile"; + +interface FormFlowRuleProps { + snssaiIndex: number; + dnn: string; + snssai: Nssai; +} + +export default function FormFlowRule({ snssaiIndex, dnn, snssai }: FormFlowRuleProps) { + const { register, validationErrors, control } = useProfileForm(); + const { + fields: flowRules, + append: appendFlowRule, + remove: removeFlowRule, + } = useFieldArray({ + control, + name: `SnssaiConfigurations.${snssaiIndex}.dnnConfigurations.${dnn}.flowRules`, + }); + + const flowKey = toHex(snssai.sst) + snssai.sd; + const idPrefix = flowKey + "-" + dnn + "-"; + + return ( + <> + {flowRules.map((flow, index) => ( +
+ + + +

Flow Rules {index + 1}

+
+ + + + + +
+ + + + + + + + + + + + + + + {/* Keep layout aligned*/} + + + + + + + + + + + + + + + + +
+ +
+
+
+ ))} + + + + + + + + +
+ + ); +} diff --git a/frontend/src/pages/ProfileCreate/FormUpSecurity.tsx b/frontend/src/pages/ProfileCreate/FormUpSecurity.tsx new file mode 100644 index 00000000..7441bf25 --- /dev/null +++ b/frontend/src/pages/ProfileCreate/FormUpSecurity.tsx @@ -0,0 +1,161 @@ +import { + Button, + Box, + Card, + FormControl, + Grid, + InputLabel, + MenuItem, + Select, + Table, + TableBody, + TableCell, + TableRow, + SelectChangeEvent, +} from "@mui/material"; +import { Controller } from "react-hook-form"; +import { useProfileForm } from "../../hooks/profile-form"; +import { defaultUpSecurity } from "../../lib/dtos/profile"; + +interface FormUpSecurityProps { + sessionIndex: number; + dnnKey: string; +} + +function NoUpSecurity(props: FormUpSecurityProps) { + const { watch, setValue } = useProfileForm(); + + const dnnConfig = watch( + `SnssaiConfigurations.${props.sessionIndex}.dnnConfigurations.${props.dnnKey}`, + ); + + const onUpSecurity = () => { + dnnConfig.upSecurity = defaultUpSecurity(); + + setValue( + `SnssaiConfigurations.${props.sessionIndex}.dnnConfigurations.${props.dnnKey}`, + dnnConfig, + ); + }; + + return ( +
+ + + + + + + + +
+
+ ); +} + +export default function FormUpSecurity(props: FormUpSecurityProps) { + const { register, validationErrors, watch, control, getValues, setValue } = useProfileForm(); + + const dnnConfig = watch( + `SnssaiConfigurations.${props.sessionIndex}.dnnConfigurations.${props.dnnKey}`, + ); + + if (!(dnnConfig.upSecurity !== undefined)) { + return ; + } + + const onUpSecurityDelete = () => { + setValue(`SnssaiConfigurations.${props.sessionIndex}.dnnConfigurations.${props.dnnKey}`, { + ...dnnConfig, + upSecurity: undefined, + }); + }; + + return ( +
+ + + +

UP Security

+
+ + + + + +
+ + + + + + + + Integrity of UP Security + ( + + )} + /> + + + + + + + + + + Confidentiality of UP Security + ( + + )} + /> + + + + +
+
+
+
+ ); +} diff --git a/frontend/src/pages/ProfileCreate/ProfileFormBasic.tsx b/frontend/src/pages/ProfileCreate/ProfileFormBasic.tsx new file mode 100644 index 00000000..ed530a39 --- /dev/null +++ b/frontend/src/pages/ProfileCreate/ProfileFormBasic.tsx @@ -0,0 +1,35 @@ +import { useProfileForm } from "../../hooks/profile-form"; +import { + Card, + Table, + TableBody, + TableCell, + TableRow, + TextField, +} from "@mui/material"; + +export default function ProfileFormBasic() { + const { register, validationErrors } = useProfileForm(); + + return ( + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/ProfileCreate/ProfileFormSessions.tsx b/frontend/src/pages/ProfileCreate/ProfileFormSessions.tsx new file mode 100644 index 00000000..7c4488a7 --- /dev/null +++ b/frontend/src/pages/ProfileCreate/ProfileFormSessions.tsx @@ -0,0 +1,375 @@ +import { + Button, + Box, + Card, + Checkbox, + Grid, + Table, + TableBody, + TableCell, + TableRow, + TextField, + Switch, +} from "@mui/material"; +import { useProfileForm } from "../../hooks/profile-form"; +import { toHex } from "../../lib/utils"; +import FormChargingConfig from "./FormCharingConfig"; +import FormFlowRule from "./FormFlowRule"; +import FormUpSecurity from "./FormUpSecurity"; +import axios from "../../axios"; +import { Controller, useFieldArray } from "react-hook-form"; +import { defaultDnnConfig, defaultSnssaiConfiguration } from "../../lib/dtos/profile"; +import { useState } from "react"; + +interface VerifyScope { + supi: string; + sd: string; + sst: number; + dnn: string; + ipaddr: string; +} + +interface VerifyResult { + ipaddr: string; + valid: boolean; + cause: string; +} + +const handleVerifyStaticIp = (sd: string, sst: number, dnn: string, ipaddr: string) => { + const scope: VerifyScope = { + supi: "", + sd: sd, + sst: sst, + dnn: dnn, + ipaddr: ipaddr, + }; + axios.post("/api/verify-staticip", scope).then((res) => { + const result = res.data as VerifyResult; + console.log(result); + if (result["valid"] === true) { + alert("OK\n" + result.ipaddr); + } else { + alert("NO!\nCause: " + result["cause"]); + } + }); +}; + +export default function ProfileFormSessions() { + const { register, validationErrors, watch, control, setFocus } = useProfileForm(); + + const { + fields: snssaiConfigurations, + append: appendSnssaiConfiguration, + remove: removeSnssaiConfiguration, + update: updateSnssaiConfiguration, + } = useFieldArray({ + control, + name: "SnssaiConfigurations", + }); + + const [dnnName, setDnnName] = useState(Array(snssaiConfigurations.length).fill("")); + + const handleChangeDNN = ( + event: React.ChangeEvent, + index: number, + ): void => { + setDnnName((dnnName) => dnnName.map((name, i) => (index === i ? event.target.value : name))); + }; + + const onDnnAdd = (index: number) => { + const name = dnnName[index]; + if (name === undefined || name === "") { + return; + } + + const snssaiConfig = watch(`SnssaiConfigurations.${index}`); + updateSnssaiConfiguration(index, { + ...snssaiConfig, + dnnConfigurations: { + ...snssaiConfig.dnnConfigurations, + [name]: defaultDnnConfig(), + }, + }); + + setTimeout(() => { + /* IMPORTANT: setFocus after rerender */ + setFocus(`SnssaiConfigurations.${index}.dnnConfigurations.${name}.sessionAmbr.uplink`); + }); + + // restore input field + setDnnName((dnnName) => dnnName.map((name, i) => (index === i ? "" : name))); + }; + + const onDnnDelete = (index: number, dnn: string, slice: string) => { + const snssaiConfig = watch(`SnssaiConfigurations.${index}`); + const newDnnConfigurations = { ...snssaiConfig.dnnConfigurations }; + delete newDnnConfigurations[dnn]; + + updateSnssaiConfiguration(index, { + ...snssaiConfig, + dnnConfigurations: newDnnConfigurations, + }); + }; + + return ( + <> + {snssaiConfigurations?.map((row, index) => ( +
+ + +

S-NSSAI Configuragtion ({toHex(row.sst) + row.sd})

+
+ + + + + +
+ + + + + + + + + + + + + + + Default S-NSSAI + + ( + + )} + /> + + + +
+ + + + {Object.keys(row.dnnConfigurations).map((dnn) => ( +
+ + + +

DNN Configurations

+
+ + + + + +
+ + + + + + {dnn} + + + + + + + + + + + + + + + + +
+ + + + + + + + + {/* + + */} + + + + + +
+ + + + +
+
+
+ ))} + + + + handleChangeDNN(ev, index)} + /> + + + + + + + + +
+
+ ))} + +
+ + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx b/frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx new file mode 100644 index 00000000..acb7b760 --- /dev/null +++ b/frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx @@ -0,0 +1,43 @@ +import { Card, Table, TableBody, TableCell, TableRow, TextField } from "@mui/material"; +import { useProfileForm } from "../../hooks/profile-form"; + +export default function ProfileFormUeAmbr() { + const { register, validationErrors } = useProfileForm(); + + return ( + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/ProfileCreate/index.tsx b/frontend/src/pages/ProfileCreate/index.tsx new file mode 100644 index 00000000..79cfa7f7 --- /dev/null +++ b/frontend/src/pages/ProfileCreate/index.tsx @@ -0,0 +1,148 @@ +import React, { useState } from "react"; +import { useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; + +import axios from "../../axios"; + +import Dashboard from "../../Dashboard"; +import { Button, Grid } from "@mui/material"; +import { ProfileFormProvider, useProfileForm } from "../../hooks/profile-form"; +import ProfileFormBasic from "./ProfileFormBasic"; +import ProfileFormUeAmbr from "./ProfileFormUeAmbr"; +import ProfileFormSessions from "./ProfileFormSessions"; +import { ProfileMapperImpl, FlowsMapperImpl } from "../../lib/dtos/profile"; +function FormHOC(Component: React.ComponentType) { + return function (props: any) { + return ( + + + + ); + }; +} + +export default FormHOC(ProfileCreate); + +function ProfileCreate() { + const { profileName } = useParams<{ profileName: string }>(); + + const isNewProfile = profileName === undefined; + console.log("trace: name", name); + console.log("trace: isNewProfile", isNewProfile); + const navigation = useNavigate(); + const [loading, setLoading] = useState(false); + + const { handleSubmit, getValues, reset } = useProfileForm(); + + if (!isNewProfile) { + useEffect(() => { + setLoading(true); + + axios + .get("/api/profile/" + name) + .then((res) => { + const profileMapper = new ProfileMapperImpl(new FlowsMapperImpl()); + const profile = profileMapper.mapFromProfile(res.data); + reset(profile); + }) + .finally(() => { + setLoading(false); + }); + }, [name]); + } + + if (loading) { + return
Loading...
; + } + + const onCreate = () => { + console.log("trace: onCreate"); + + const data = getValues(); + + if (data.SnssaiConfigurations.length === 0) { + alert("Please add at least one S-NSSAI"); + return; + } + + const profileMapper = new ProfileMapperImpl(new FlowsMapperImpl()); + const profile = profileMapper.mapFromDto(data); + + axios + .post("/api/profile", profile) + .then(() => { + navigation("/profile"); + }) + .catch((err) => { + if (err.response) { + const msg = "Status: " + err.response.status; + if (err.response.data.cause) { + alert(msg + ", cause: " + err.response.data.cause); + } else if (err.response.data) { + alert(msg + ", data:" + err.response.data); + } else { + alert(msg); + } + } else { + alert(err.message); + } + console.log(err); + return; + }); + }; + + const onUpdate = () => { + console.log("trace: onUpdate"); + + const data = getValues(); + const profileMapper = new ProfileMapperImpl(new FlowsMapperImpl()); + const profile = profileMapper.mapFromDto(data); + + axios + .put("/api/profile/" + profile.profileName, profile) + .then(() => { + navigation("/profile/" + profile.profileName); + }) + .catch((err) => { + if (err.response) { + const msg = "Status: " + err.response.status; + if (err.response.data.cause) { + alert(msg + ", cause: " + err.response.data.cause); + } else if (err.response.data) { + alert(msg + ", data:" + err.response.data); + } else { + alert(msg); + } + } + }); + }; + + const formSubmitFn = isNewProfile ? onCreate : onUpdate; + const formSubmitText = isNewProfile ? "CREATE" : "UPDATE"; + + return ( + {}}> +
{ + console.log("form error: ", err); + })} + > + + + +

Profile UE AMBR

+ + + + +
+ + + + +
+ ); +} + diff --git a/frontend/src/pages/ProfileList.tsx b/frontend/src/pages/ProfileList.tsx new file mode 100644 index 00000000..b90eadb4 --- /dev/null +++ b/frontend/src/pages/ProfileList.tsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import Dashboard from "../Dashboard"; +import axios from "../axios"; +import { Profile } from "../api/api"; +import { + Button, + Grid, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@mui/material"; + +export default function ProfileList() { + const navigation = useNavigate(); + const [refresh, setRefresh] = useState(false); + const [data, setData] = useState([]); + const [limit, setLimit] = useState(50); + const [page, setPage] = useState(0); + + useEffect(() => { + console.log("get profiles"); + axios + .get("/api/profile") + .then((res) => { + setData(res.data); + }) + .catch((e) => { + console.log(e.message); + }); + // set data to empty array + // setData([]); + }, [refresh, limit, page]); + + const onCreate = () => { + navigation("/profile/create"); + }; + + const onDelete = (profileName: string) => { + const result = window.confirm("Delete profile?"); + if (!result) { + return; + } + axios + .delete("/api/profile/" + profileName) + .then((res) => { + console.log(res); + setRefresh(!refresh); + }) + .catch((err) => { + console.log(err.response.data.message); + }); + }; + + const handleModify = (profile: Profile) => { + navigation("/profile/" + profile.profileName); + }; + + const tableView = ( + + + + + Name + Delete + View + + + + {data.map((row, index) => ( + + {row.profileName} + + + + + + + + ))} + +
+ + + +
+ ); + + return ( + setRefresh(!refresh)}> +
+ {data.length === 0 ? ( +
+ No Profiles +
+
+ + + +
+ ) : ( + tableView + )} +
+ ); +} diff --git a/frontend/src/pages/ProfileRead.tsx b/frontend/src/pages/ProfileRead.tsx new file mode 100644 index 00000000..b4cbdc0e --- /dev/null +++ b/frontend/src/pages/ProfileRead.tsx @@ -0,0 +1,345 @@ +import React from "react"; +import { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; + +import axios from "../axios"; +import { + Nssai, + Profile, + QosFlows, + DnnConfiguration +} from "../api/api"; +import Dashboard from "../Dashboard"; +import { + Button, + Box, + Card, + Checkbox, + Grid, + Table, + TableBody, + TableCell, + TableRow, +} from "@mui/material"; + +export default function ProfileRead() { + const { profileName } = useParams<{ profileName: string }>(); + const navigation = useNavigate(); + + const [data, setData] = useState(null); + + function toHex(v: number | undefined): string { + return ("00" + v?.toString(16).toUpperCase()).substr(-2); + } + + useEffect(() => { + axios.get("/api/profile/" + profileName).then((res) => { + setData(res.data); + }); + }, [profileName]); + + const handleEdit = () => { + navigation("/profile/create/" + profileName); + }; + + const isDefaultNssai = (nssai: Nssai | undefined) => { + if (nssai === undefined || data == null) { + return false; + } else { + for ( + let i = 0; + i < data.AccessAndMobilitySubscriptionData.nssai!.defaultSingleNssais!.length; + i++ + ) { + const defaultNssai = data.AccessAndMobilitySubscriptionData.nssai!.defaultSingleNssais![i]; + if (defaultNssai.sd === nssai.sd && defaultNssai.sst === nssai.sst) { + return true; + } + } + return false; + } + } + + const qosFlow = ( + sstSd: string, + dnn: string, + qosRef: number | undefined, + ): QosFlows | undefined => { + if (data != null) { + for (const qos of data.QosFlows) { + if (qos.snssai === sstSd && qos.dnn === dnn && qos.qosRef === qosRef) { + return qos; + } + } + } + return undefined; + } + + const chargingConfig = (dnn: string, snssai: Nssai, filter: string | undefined) => { + const flowKey = toHex(snssai.sst) + snssai.sd; + for (const chargingData of data?.ChargingDatas ?? []) { + const isOnlineCharging = chargingData.chargingMethod === "Online"; + + if ( + chargingData.snssai === flowKey && + chargingData.dnn === dnn && + chargingData.filter === filter + ) { + return ( + + + +

Charging Config

+
+
+ + + + Charging Method + {chargingData.chargingMethod} + + + {isOnlineCharging ? ( + + + Quota + {chargingData.quota} + + + ) : ( + <> + )} + + Unit Cost + {chargingData.unitCost} + +
+
+ ); + } + } + }; + + const flowRule = (dnn: string, snssai: Nssai) => { + console.log("in flowRule"); + console.log(data?.FlowRules); + const flowKey = toHex(snssai.sst) + snssai.sd; + if (data?.FlowRules !== undefined) { + return data.FlowRules.filter((flow) => flow.dnn === dnn && flow.snssai === flowKey).map( + (flow) => ( +
+ +

Flow Rules

+ + + + + IP Filter + {flow.filter} + + + + + Precedence + {flow.precedence} + + + + + 5QI + {qosFlow(flowKey, dnn, flow.qosRef)?.["5qi"]} + + + + + Uplink GBR + {qosFlow(flowKey, dnn, flow.qosRef!)?.gbrUL} + + + + + Downlink GBR + {qosFlow(flowKey, dnn, flow.qosRef!)?.gbrDL} + + + + + Uplink MBR + {qosFlow(flowKey, dnn, flow.qosRef!)?.mbrUL} + + + + + Downlink MBR + {qosFlow(flowKey, dnn, flow.qosRef!)?.mbrDL} + + + + + Charging Characteristics + {chargingConfig(dnn, snssai!, flow.filter)} + + +
+
+
+
+ ), + ); + } + return
; + }; + + const upSecurity = (dnn: DnnConfiguration | undefined) => { + if (dnn !== undefined && dnn!.upSecurity !== undefined) { + const security = dnn!.upSecurity!; + return ( +
+ +

UP Security

+ + + + + Integrity of UP Security + {security.upIntegr} + + + + + Confidentiality of UP Security + {security.upConfid} + + +
+
+
+
+ ); + } else { + return
; + } + }; + + return ( + {}}> + + + + + Profile Name + {data?.profileName} + + +
+
+ +

Subscribed UE AMBR

+ + + + + Uplink + {data?.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.uplink} + + + + + Downlink + {data?.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.downlink} + + +
+
+ + {/* S-NSSAI Configurations */} + {data?.SessionManagementSubscriptionData?.map((row, index) => ( +
+

S-NSSAI Configuration

+ + + + + SST + {row.singleNssai?.sst} + + + + + SD + {row.singleNssai?.sd} + + + + + Default S-NSSAI + + + + + +
+ {row.dnnConfigurations && + Object.keys(row.dnnConfigurations!).map((dnn) => ( +
+ +

DNN Configurations

+ + + + + Data Network Name + {dnn} + + + + + Uplink AMBR + {row.dnnConfigurations![dnn].sessionAmbr?.uplink} / {row.dnnConfigurations![dnn].sessionAmbr?.downlink} + + + + + Downlink AMBR + {row.dnnConfigurations![dnn].sessionAmbr?.downlink} + + + + + Default 5QI + {row.dnnConfigurations![dnn]["5gQosProfile"]?.["5qi"]} + + + + + Static IPv4 Address + + {row.dnnConfigurations![dnn]["staticIpAddress"] == null + ? "Not Set" + : row.dnnConfigurations![dnn]["staticIpAddress"]?.length == 0 + ? "" + : row.dnnConfigurations![dnn]["staticIpAddress"]![0].ipv4Addr!} + + + +
+ {flowRule(dnn, row.singleNssai!)} + {upSecurity(row.dnnConfigurations![dnn])} + {chargingConfig("", row.singleNssai!, "")} +
+
+
+ ))} +
+
+ ))} + +
+ + + +
+ ); +} diff --git a/frontend/webconsole.yaml b/frontend/webconsole.yaml index b6c02fdc..cd0cbe71 100644 --- a/frontend/webconsole.yaml +++ b/frontend/webconsole.yaml @@ -36,6 +36,32 @@ paths: type: array items: $ref: "#/components/schemas/Subscriber" + /api/profile: + get: + tags: + - webconsole + summary: Get all profiles + description: Returns an array of profile. + parameters: + - in: query + name: limit + schema: + type: integer + format: int64 + - in: query + name: page + schema: + type: integer + format: int64 + responses: + "200": + description: Returns an array of profile. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ProfileListIE" components: schemas: # Meta object @@ -67,6 +93,15 @@ components: type: string gpsi: type: string + # ProfileListIE + # github.com/free5gc/webconsole/backend/WebUI/model_profile_list_ie.go:ProfileListIE + ProfileListIE: + type: object + properties: + profileName: + type: string + gpsi: + type: string # PduSession # github.com/free5gc/amf/internal/sbi/producer/oam.go:PduSession PduSession: @@ -712,3 +747,44 @@ components: type: string DlVol: type: string + # Profile + # github.com/free5gc/webconsole/backend/WebUI/model_profile_data.go:ProfileData + Profile: + type: object + required: + - profileName + - AccessAndMobilitySubscriptionData + - SessionManagementSubscriptionData + - SmfSelectionSubscriptionData + - AmPolicyData + - SmPolicyData + - FlowRules + - QosFlows + - ChargingDatas + properties: + profileName: + type: string + AccessAndMobilitySubscriptionData: + $ref: "#/components/schemas/AccessAndMobilitySubscriptionData" + SessionManagementSubscriptionData: + type: array + items: + $ref: "#/components/schemas/SessionManagementSubscriptionData" + SmfSelectionSubscriptionData: + $ref: "#/components/schemas/SmfSelectionSubscriptionData" + AmPolicyData: + $ref: "#/components/schemas/AmPolicyData" + SmPolicyData: + $ref: "#/components/schemas/SmPolicyData" + FlowRules: + type: array + items: + $ref: "#/components/schemas/FlowRules" + QosFlows: + type: array + items: + $ref: "#/components/schemas/QosFlows" + ChargingDatas: + type: array + items: + $ref: "#/components/schemas/ChargingData" From 13431ff41cbd0bdc2fb6885b4c6b13fe1be09c66 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Mon, 28 Oct 2024 13:42:09 +0000 Subject: [PATCH 06/28] feat: add a script for quickly compile frontend and run backend --- run.sh | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 run.sh diff --git a/run.sh b/run.sh new file mode 100755 index 00000000..75b9ba20 --- /dev/null +++ b/run.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +######################################################## +# +# This script is used to build and run the free5gc-Webconsole +# +# For quickly developing used +# +########## + +cd frontend + +# check yarn install +if [ ! -d "node_modules" ]; then + echo "node_modules not found, installing..." + yarn install +else + echo "node_modules found, skipping installation" +fi + +# yarn build +echo "building frontend..." +yarn build + +# copy build to public +echo "copying build to public..." +rm -rf ../public +cp -R build ../public +cd .. + +# run server +echo "running server..." +go run server.go From c35d1d2ea090fcd0e946391326a9d6dbc66bbca6 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Wed, 30 Oct 2024 05:51:40 +0000 Subject: [PATCH 07/28] feat: add search box and pager in profile mlist page --- frontend/src/pages/ProfileList.tsx | 60 ++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/ProfileList.tsx b/frontend/src/pages/ProfileList.tsx index b90eadb4..bc6c8642 100644 --- a/frontend/src/pages/ProfileList.tsx +++ b/frontend/src/pages/ProfileList.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; +import { config } from "../constants/config"; import Dashboard from "../Dashboard"; import axios from "../axios"; import { Profile } from "../api/api"; @@ -10,7 +11,9 @@ import { TableBody, TableCell, TableHead, + TablePagination, TableRow, + TextField, } from "@mui/material"; export default function ProfileList() { @@ -19,6 +22,7 @@ export default function ProfileList() { const [data, setData] = useState([]); const [limit, setLimit] = useState(50); const [page, setPage] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); useEffect(() => { console.log("get profiles"); @@ -30,10 +34,43 @@ export default function ProfileList() { .catch((e) => { console.log(e.message); }); - // set data to empty array - // setData([]); }, [refresh, limit, page]); + const handlePageChange = ( + _event: React.MouseEvent | null, + newPage?: number, + ) => { + if (newPage !== null) { + setPage(newPage!); + } + }; + + const handleLimitChange = (event: React.ChangeEvent) => { + setLimit(Number(event.target.value)); + }; + + const count = () => { + return 0; + }; + + const pager = () => { + if (config.enablePagination) { + return ( + + ); + } else { + return
; + } + }; + const onCreate = () => { navigation("/profile/create"); }; @@ -58,8 +95,24 @@ export default function ProfileList() { navigation("/profile/" + profile.profileName); }; + const filteredData = data.filter((profile) => + profile.profileName?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleSearch = (event: React.ChangeEvent) => { + setSearchTerm(event.target.value); + }; + const tableView = ( + @@ -69,7 +122,7 @@ export default function ProfileList() { - {data.map((row, index) => ( + {filteredData.map((row, index) => ( {row.profileName} @@ -86,6 +139,7 @@ export default function ProfileList() { ))}
+ {pager()} + + + + + + + + + + + + + + + + + + Default S-NSSAI + + } + /> + + + +
- updateSnssaiConfiguration(index, { - ...snssaiConfig, - dnnConfigurations: newDnnConfigurations, - }); - }; + - return ( - <> - {snssaiConfigurations?.map((row, index) => ( -
- + {Object.keys(row.dnnConfigurations).map((dnn) => ( +
+ + -

S-NSSAI Configuragtion ({toHex(row.sst) + row.sd})

+

DNN Configurations

- - - - -
- - - + + + + +
+ - - - - - - + + {dnn} + - - + + - Default S-NSSAI - - ( - - )} + + - + + + + + + + - -
- - + + - {Object.keys(row.dnnConfigurations).map((dnn) => ( -
- - - -

DNN Configurations

-
- - - - - -
- - - - - - {dnn} - - - - - - - - - - - - - - - - -
- - - - - - - +
+ + + + + - {/* + {/* */} - - - - - -
- - - - -
-
-
- ))} - - - - handleChangeDNN(ev, index)} - /> - - - - + - - - -
-
- ))} + + + + -
- -
+ ))} + + + + handleChangeDNN(ev, index)} + /> + + + + + + onClick={() => onDnnAdd(index)} + sx={{ m: 3 }} + > +   +DNN   + + + - - ); -} \ No newline at end of file +
+ + ))} + +
+ + + + + ); +} diff --git a/frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx b/frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx index acb7b760..9464a0a9 100644 --- a/frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx +++ b/frontend/src/pages/ProfileCreate/ProfileFormUeAmbr.tsx @@ -2,42 +2,42 @@ import { Card, Table, TableBody, TableCell, TableRow, TextField } from "@mui/mat import { useProfileForm } from "../../hooks/profile-form"; export default function ProfileFormUeAmbr() { - const { register, validationErrors } = useProfileForm(); + const { register, validationErrors } = useProfileForm(); - return ( - - - - - - - - - - - - -
-
- ); + return ( + + + + + + + + + + + + +
+
+ ); } diff --git a/frontend/src/pages/ProfileRead.tsx b/frontend/src/pages/ProfileRead.tsx index 308bbb4b..667a98f7 100644 --- a/frontend/src/pages/ProfileRead.tsx +++ b/frontend/src/pages/ProfileRead.tsx @@ -3,240 +3,246 @@ import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import axios from "../axios"; -import { - Nssai, - Profile, - QosFlows, - DnnConfiguration -} from "../api/api"; +import { Nssai, Profile, QosFlows, DnnConfiguration } from "../api/api"; import Dashboard from "../Dashboard"; import { - Button, - Box, - Card, - Checkbox, - Grid, - Table, - TableBody, - TableCell, - TableRow, + Button, + Box, + Card, + Checkbox, + Grid, + Table, + TableBody, + TableCell, + TableRow, } from "@mui/material"; import FlowRule from "./Component/FlowRule"; import ChargingCfg from "./Component/ChargingCfg"; import UpSecurity from "./Component/UpSecurity"; export default function ProfileRead() { - const { profileName } = useParams<{ profileName: string }>(); - const navigation = useNavigate(); - - const [data, setData] = useState(null); - - function toHex(v: number | undefined): string { - return ("00" + v?.toString(16).toUpperCase()).substr(-2); - } - - useEffect(() => { - axios.get("/api/profile/" + profileName).then((res) => { - setData(res.data); - }); - }, [profileName]); - - const handleEdit = () => { - navigation("/profile/create/" + profileName); - }; - - const isDefaultNssai = (nssai: Nssai | undefined) => { - if (nssai === undefined || data == null) { - return false; - } else { - for ( - let i = 0; - i < data.AccessAndMobilitySubscriptionData.nssai!.defaultSingleNssais!.length; - i++ - ) { - const defaultNssai = data.AccessAndMobilitySubscriptionData.nssai!.defaultSingleNssais![i]; - if (defaultNssai.sd === nssai.sd && defaultNssai.sst === nssai.sst) { - return true; - } - } - return false; + const { profileName } = useParams<{ profileName: string }>(); + const navigation = useNavigate(); + + const [data, setData] = useState(null); + + function toHex(v: number | undefined): string { + return ("00" + v?.toString(16).toUpperCase()).substr(-2); + } + + useEffect(() => { + axios.get("/api/profile/" + profileName).then((res) => { + setData(res.data); + }); + }, [profileName]); + + const handleEdit = () => { + navigation("/profile/create/" + profileName); + }; + + const isDefaultNssai = (nssai: Nssai | undefined) => { + if (nssai === undefined || data == null) { + return false; + } else { + for ( + let i = 0; + i < data.AccessAndMobilitySubscriptionData.nssai!.defaultSingleNssais!.length; + i++ + ) { + const defaultNssai = data.AccessAndMobilitySubscriptionData.nssai!.defaultSingleNssais![i]; + if (defaultNssai.sd === nssai.sd && defaultNssai.sst === nssai.sst) { + return true; } + } + return false; } - - const qosFlow = ( - sstSd: string, - dnn: string, - qosRef: number | undefined, - ): QosFlows | undefined => { - if (data != null) { - for (const qos of data.QosFlows) { - if (qos.snssai === sstSd && qos.dnn === dnn && qos.qosRef === qosRef) { - return qos; - } - } + }; + + const qosFlow = ( + sstSd: string, + dnn: string, + qosRef: number | undefined, + ): QosFlows | undefined => { + if (data != null) { + for (const qos of data.QosFlows) { + if (qos.snssai === sstSd && qos.dnn === dnn && qos.qosRef === qosRef) { + return qos; } - return undefined; + } + } + return undefined; + }; + + const chargingConfig = (dnn: string, snssai: Nssai, filter: string | undefined) => { + const flowKey = toHex(snssai.sst) + snssai.sd; + for (const chargingData of data?.ChargingDatas ?? []) { + if ( + chargingData.snssai === flowKey && + chargingData.dnn === dnn && + chargingData.filter === filter + ) { + return ; + } } + }; - const chargingConfig = (dnn: string, snssai: Nssai, filter: string | undefined) => { - const flowKey = toHex(snssai.sst) + snssai.sd; - for (const chargingData of data?.ChargingDatas ?? []) { - if ( - chargingData.snssai === flowKey && - chargingData.dnn === dnn && - chargingData.filter === filter - ) { - return ; - } - } - }; + const flowRule = (dnn: string, snssai: Nssai) => { + const flowKey = toHex(snssai.sst) + snssai.sd; + if (data?.FlowRules === undefined) { + return
; + } + return data.FlowRules.filter((flow) => flow.dnn === dnn && flow.snssai === flowKey).map( + (flow) => ( + + ), + ); + }; - const flowRule = (dnn: string, snssai: Nssai) => { - const flowKey = toHex(snssai.sst) + snssai.sd; - if (data?.FlowRules === undefined) { - return
; - } - return data.FlowRules.filter((flow) => flow.dnn === dnn && flow.snssai === flowKey).map( - (flow) => ( - - ), - ); - }; - - const upSecurity = (dnn: DnnConfiguration | undefined) => { - if (dnn === undefined || dnn.upSecurity === undefined) { - return
; - } - return ; - }; - - return ( - {}}> - - - - - Profile Name - {data?.profileName} - - -
-
- -

Subscribed UE AMBR

- - - - - Uplink - {data?.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.uplink} - - - - - Downlink - {data?.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.downlink} - - -
-
- - {/* S-NSSAI Configurations */} - {data?.SessionManagementSubscriptionData?.map((row, index) => ( -
-

S-NSSAI Configuration

+ const upSecurity = (dnn: DnnConfiguration | undefined) => { + if (dnn === undefined || dnn.upSecurity === undefined) { + return
; + } + return ; + }; + + return ( + {}}> + + + + + Profile Name + {data?.profileName} + + +
+
+ +

Subscribed UE AMBR

+ + + + + Uplink + + {data?.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.uplink} + + + + + + Downlink + + {data?.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.downlink} + + + +
+
+ + {/* S-NSSAI Configurations */} + {data?.SessionManagementSubscriptionData?.map((row, index) => ( +
+

S-NSSAI Configuration

+ + + + + SST + {row.singleNssai?.sst} + + + + + SD + {row.singleNssai?.sd} + + + + + Default S-NSSAI + + + + + +
+ {row.dnnConfigurations && + Object.keys(row.dnnConfigurations!).map((dnn) => ( +
+ +

DNN Configurations

- - - - SST - {row.singleNssai?.sst} - - - - - SD - {row.singleNssai?.sd} - - - - - Default S-NSSAI - - - - - -
- {row.dnnConfigurations && - Object.keys(row.dnnConfigurations!).map((dnn) => ( -
- -

DNN Configurations

- - - - - Data Network Name - {dnn} - - - - - Uplink AMBR - {row.dnnConfigurations![dnn].sessionAmbr?.uplink} / {row.dnnConfigurations![dnn].sessionAmbr?.downlink} - - - - - Downlink AMBR - {row.dnnConfigurations![dnn].sessionAmbr?.downlink} - - - - - Default 5QI - {row.dnnConfigurations![dnn]["5gQosProfile"]?.["5qi"]} - - - - - Static IPv4 Address - - {row.dnnConfigurations![dnn]["staticIpAddress"] == null - ? "Not Set" - : row.dnnConfigurations![dnn]["staticIpAddress"]?.length == 0 - ? "" - : row.dnnConfigurations![dnn]["staticIpAddress"]![0].ipv4Addr!} - - - -
- {flowRule(dnn, row.singleNssai!)} - {upSecurity(row.dnnConfigurations![dnn])} - {chargingConfig("", row.singleNssai!, "")} -
-
-
- ))} + + + + Data Network Name + {dnn} + + + + + Uplink AMBR + + {row.dnnConfigurations![dnn].sessionAmbr?.uplink} /{" "} + {row.dnnConfigurations![dnn].sessionAmbr?.downlink} + + + + + + Downlink AMBR + + {row.dnnConfigurations![dnn].sessionAmbr?.downlink} + + + + + + Default 5QI + + {row.dnnConfigurations![dnn]["5gQosProfile"]?.["5qi"]} + + + + + + Static IPv4 Address + + {row.dnnConfigurations![dnn]["staticIpAddress"] == null + ? "Not Set" + : row.dnnConfigurations![dnn]["staticIpAddress"]?.length == 0 + ? "" + : row.dnnConfigurations![dnn]["staticIpAddress"]![0].ipv4Addr!} + + + +
+ {flowRule(dnn, row.singleNssai!)} + {upSecurity(row.dnnConfigurations![dnn])} + {chargingConfig("", row.singleNssai!, "")}
+
- ))} - -
- - - - - ); + ))} +
+
+ ))} + +
+ + + +
+ ); } From 6d6a95b5a3d1d0185634a9260dd897203c659bf0 Mon Sep 17 00:00:00 2001 From: yccodr Date: Tue, 5 Nov 2024 11:13:16 +0800 Subject: [PATCH 20/28] chore: format --- frontend/src/pages/ProfileList.tsx | 296 +++++++++++++++-------------- 1 file changed, 150 insertions(+), 146 deletions(-) diff --git a/frontend/src/pages/ProfileList.tsx b/frontend/src/pages/ProfileList.tsx index 14841d2b..c87a1c1a 100644 --- a/frontend/src/pages/ProfileList.tsx +++ b/frontend/src/pages/ProfileList.tsx @@ -5,164 +5,168 @@ import Dashboard from "../Dashboard"; import axios from "../axios"; import { Profile } from "../api/api"; import { - Button, - Grid, - Table, - TableBody, - TableCell, - TableHead, - TablePagination, - TableRow, - TextField, + Button, + Grid, + Table, + TableBody, + TableCell, + TableHead, + TablePagination, + TableRow, + TextField, } from "@mui/material"; export default function ProfileList() { - const navigation = useNavigate(); - const [refresh, setRefresh] = useState(false); - const [data, setData] = useState([]); - const [limit, setLimit] = useState(50); - const [page, setPage] = useState(0); - const [searchTerm, setSearchTerm] = useState(""); + const navigation = useNavigate(); + const [refresh, setRefresh] = useState(false); + const [data, setData] = useState([]); + const [limit, setLimit] = useState(50); + const [page, setPage] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); - useEffect(() => { - axios - .get("/api/profile") - .then((res) => { - setData(res.data); - }) - .catch((e) => { - alert(e.message); - }); - }, [refresh, limit, page]); + useEffect(() => { + axios + .get("/api/profile") + .then((res) => { + setData(res.data); + }) + .catch((e) => { + alert(e.message); + }); + }, [refresh, limit, page]); - const handlePageChange = ( - _event: React.MouseEvent | null, - newPage?: number, - ) => { - if (newPage !== null) { - setPage(newPage!); - } - }; + const handlePageChange = ( + _event: React.MouseEvent | null, + newPage?: number, + ) => { + if (newPage !== null) { + setPage(newPage!); + } + }; - const handleLimitChange = (event: React.ChangeEvent) => { - setLimit(Number(event.target.value)); - }; - - const count = () => { - return 0; - }; - - const pager = () => { - if (config.enablePagination) { - return ( - - ); - } else { - return
; - } - }; + const handleLimitChange = (event: React.ChangeEvent) => { + setLimit(Number(event.target.value)); + }; - const onCreate = () => { - navigation("/profile/create"); - }; + const count = () => { + return 0; + }; - const onDelete = (profileName: string) => { - const result = window.confirm("Delete profile?"); - if (!result) { - return; - } - axios - .delete("/api/profile/" + profileName) - .then((res) => { - setRefresh(!refresh); - }) - .catch((err) => { - alert(err.response.data.message); - }); - }; + const pager = () => { + if (config.enablePagination) { + return ( + + ); + } else { + return
; + } + }; - const handleModify = (profile: Profile) => { - navigation("/profile/" + profile.profileName); - }; + const onCreate = () => { + navigation("/profile/create"); + }; - const filteredData = data.filter((profile) => - profile.profileName?.toLowerCase().includes(searchTerm.toLowerCase()) - ); + const onDelete = (profileName: string) => { + const result = window.confirm("Delete profile?"); + if (!result) { + return; + } + axios + .delete("/api/profile/" + profileName) + .then((res) => { + setRefresh(!refresh); + }) + .catch((err) => { + alert(err.response.data.message); + }); + }; - const handleSearch = (event: React.ChangeEvent) => { - setSearchTerm(event.target.value); - }; + const handleModify = (profile: Profile) => { + navigation("/profile/" + profile.profileName); + }; - const tableView = ( - - - - - - Name - Delete - View - - - - {filteredData.map((row, index) => ( - - {row.profileName} - - - - - - - - ))} - -
- {pager()} - - + +
+ ); - return ( - setRefresh(!refresh)}> -
- {data.length === 0 ? ( -
- No Profiles -
-
- - - -
- ) : ( - tableView - )} -
- ); + return ( + setRefresh(!refresh)}> +
+ {data.length === 0 ? ( +
+ No Profiles +
+
+ + + +
+ ) : ( + tableView + )} +
+ ); } From e1150e09a3c5b16e4ca7abd078bc3e68474ff5a8 Mon Sep 17 00:00:00 2001 From: yccodr Date: Tue, 5 Nov 2024 12:03:51 +0800 Subject: [PATCH 21/28] feat: show more friendly ui when error occurs --- frontend/src/pages/ProfileList.tsx | 60 ++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/ProfileList.tsx b/frontend/src/pages/ProfileList.tsx index c87a1c1a..2c315681 100644 --- a/frontend/src/pages/ProfileList.tsx +++ b/frontend/src/pages/ProfileList.tsx @@ -5,8 +5,12 @@ import Dashboard from "../Dashboard"; import axios from "../axios"; import { Profile } from "../api/api"; import { + Alert, + Box, Button, Grid, + Snackbar, + Stack, Table, TableBody, TableCell, @@ -15,14 +19,21 @@ import { TableRow, TextField, } from "@mui/material"; +import { ReportProblemRounded } from "@mui/icons-material"; -export default function ProfileList() { +interface Props { + refresh: boolean; + setRefresh: (v: boolean) => void; +} + +function ProfileList(props: Props) { const navigation = useNavigate(); - const [refresh, setRefresh] = useState(false); const [data, setData] = useState([]); const [limit, setLimit] = useState(50); const [page, setPage] = useState(0); const [searchTerm, setSearchTerm] = useState(""); + const [isLoadError, setIsLoadError] = useState(false); + const [isDeleteError, setIsDeleteError] = useState(false); useEffect(() => { axios @@ -31,9 +42,18 @@ export default function ProfileList() { setData(res.data); }) .catch((e) => { - alert(e.message); + setIsLoadError(true); }); - }, [refresh, limit, page]); + }, [props.refresh, limit, page]); + + if (isLoadError) { + return ( + + + Something went wrong + + ); + } const handlePageChange = ( _event: React.MouseEvent | null, @@ -82,10 +102,11 @@ export default function ProfileList() { axios .delete("/api/profile/" + profileName) .then((res) => { - setRefresh(!refresh); + props.setRefresh(!props.refresh); }) .catch((err) => { - alert(err.response.data.message); + setIsDeleteError(true); + console.error(err.response.data.message); }); }; @@ -147,11 +168,20 @@ export default function ProfileList() { CREATE + + setIsDeleteError(false)} + > + Failed to delete profile + ); return ( - setRefresh(!refresh)}> + <>
{data.length === 0 ? (
@@ -167,6 +197,20 @@ export default function ProfileList() { ) : ( tableView )} - + ); } + +function WithDashboard(Component: React.ComponentType) { + return function (props: any) { + const [refresh, setRefresh] = useState(false); + + return ( + setRefresh(!refresh)}> + setRefresh(v)} /> + + ); + }; +} + +export default WithDashboard(ProfileList); From ba474d60a191979b7e38e7c9c1def8afefd20111 Mon Sep 17 00:00:00 2001 From: yccodr Date: Tue, 5 Nov 2024 12:06:47 +0800 Subject: [PATCH 22/28] chore: early return on no profile found --- frontend/src/pages/ProfileList.tsx | 43 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/frontend/src/pages/ProfileList.tsx b/frontend/src/pages/ProfileList.tsx index 2c315681..e34b6293 100644 --- a/frontend/src/pages/ProfileList.tsx +++ b/frontend/src/pages/ProfileList.tsx @@ -122,8 +122,27 @@ function ProfileList(props: Props) { setSearchTerm(event.target.value); }; - const tableView = ( - + if (data.length === 0) { + return ( + <> +
+
+ No Profiles +
+
+ + + +
+ + ); + } + + return ( + <> +
Failed to delete profile -
- ); - - return ( - <> -
- {data.length === 0 ? ( -
- No Profiles -
-
- - - -
- ) : ( - tableView - )} ); } From d66ffb5377651cdcefdbfd3a4901a74af96a9e49 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 8 Nov 2024 12:45:12 +0000 Subject: [PATCH 23/28] feat: make return message more clear on porfile part --- backend/WebUI/api_webui.go | 46 ++++++++++++++------------------------ 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/backend/WebUI/api_webui.go b/backend/WebUI/api_webui.go index 59d2b430..266cdf08 100644 --- a/backend/WebUI/api_webui.go +++ b/backend/WebUI/api_webui.go @@ -1964,18 +1964,16 @@ func DeleteProfile(c *gin.Context) { pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profileName}) if err != nil { logger.ProcLog.Errorf("DeleteProfile err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } if len(pf) == 0 { - c.JSON(http.StatusNotFound, gin.H{ - "cause": "Profile does not exist", - }) + c.JSON(http.StatusNotFound, "Profile does not exist") return } dbProfileOperation(profileName, "delete", nil) - c.JSON(http.StatusNoContent, gin.H{}) + c.JSON(http.StatusOK, gin.H{"success": profileName + " has already been deleted"}) } // Get profile list @@ -1986,9 +1984,7 @@ func GetProfiles(c *gin.Context) { _, err := GetTenantId(c) if err != nil { logger.ProcLog.Errorln(err.Error()) - c.JSON(http.StatusBadRequest, gin.H{ - "cause": "Illegal Token", - }) + c.JSON(http.StatusBadRequest, gin.H{"cause": "Illegal Token"}) return } @@ -1996,7 +1992,7 @@ func GetProfiles(c *gin.Context) { profileList, err := mongoapi.RestfulAPIGetMany(profileListColl, bson.M{}) if err != nil { logger.ProcLog.Errorf("GetProfiles err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } for _, profile := range profileList { @@ -2022,14 +2018,14 @@ func GetProfile(c *gin.Context) { profile, err := mongoapi.RestfulAPIGetOne(profileDataColl, bson.M{"profileName": profileName}) if err != nil { logger.ProcLog.Errorf("GetProfile err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } var pf Profile err = json.Unmarshal(mapToByte(profile), &pf) if err != nil { logger.ProcLog.Errorf("JSON Unmarshal err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } c.JSON(http.StatusOK, pf) @@ -2044,25 +2040,21 @@ func PostProfile(c *gin.Context) { _, err := ParseJWT(tokenStr) if err != nil { logger.ProcLog.Errorln(err.Error()) - c.JSON(http.StatusBadRequest, gin.H{ - "cause": "Illegal Token", - }) + c.JSON(http.StatusBadRequest, gin.H{"cause": "Illegal Token"}) return } var profile Profile if err = c.ShouldBindJSON(&profile); err != nil { logger.ProcLog.Errorf("PostProfile err: %+v", err) - c.JSON(http.StatusBadRequest, gin.H{ - "cause": "JSON format incorrect", - }) + c.JSON(http.StatusBadRequest, gin.H{"cause": "JSON format incorrect"}) return } tenantData, err := mongoapi.RestfulAPIGetOne(tenantDataColl, bson.M{"tenantName": "admin"}) if err != nil { logger.ProcLog.Errorf("GetProfile err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } profile.TenantId = tenantData["tenantId"].(string) @@ -2070,13 +2062,11 @@ func PostProfile(c *gin.Context) { pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profile.ProfileName}) if err != nil { logger.ProcLog.Errorf("GetProfile err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } if len(pf) != 0 { - c.JSON(http.StatusConflict, gin.H{ - "cause": "Profile already exists", - }) + c.JSON(http.StatusConflict, "Profile already exists") return } @@ -2095,34 +2085,32 @@ func PutProfile(c *gin.Context) { var profile Profile if err := c.ShouldBindJSON(&profile); err != nil { logger.ProcLog.Errorf("PutProfile err: %+v", err) - c.JSON(http.StatusBadRequest, gin.H{}) + c.JSON(http.StatusBadRequest, gin.H{"cause": "JSON format incorrect"}) return } pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profile.ProfileName}) if err != nil { logger.ProcLog.Errorf("PutProfile err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } if len(pf) == 0 { - c.JSON(http.StatusNotFound, gin.H{ - "cause": "Profile does not exist", - }) + c.JSON(http.StatusNotFound, "Profile does not exist") return } tenantData, err := mongoapi.RestfulAPIGetOne(tenantDataColl, bson.M{"tenantName": "admin"}) if err != nil { logger.ProcLog.Errorf("GetProfile err: %+v", err) - c.JSON(http.StatusInternalServerError, gin.H{}) + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) return } profile.TenantId = tenantData["tenantId"].(string) logger.ProcLog.Infof("PutProfile: %+v", profile.ProfileName) dbProfileOperation(profileName, "put", &profile) - c.JSON(http.StatusNoContent, gin.H{}) + c.JSON(http.StatusOK, gin.H{"success": profileName + " has already been updated"}) } func dbProfileOperation(profileName string, method string, profile *Profile) { From e503852e7a882b45c0c637683320f1d713741d7d Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 8 Nov 2024 13:03:26 +0000 Subject: [PATCH 24/28] feat: improve profile confict and not found return message's behavior --- backend/WebUI/api_webui.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/WebUI/api_webui.go b/backend/WebUI/api_webui.go index 266cdf08..4c50ed04 100644 --- a/backend/WebUI/api_webui.go +++ b/backend/WebUI/api_webui.go @@ -1968,7 +1968,7 @@ func DeleteProfile(c *gin.Context) { return } if len(pf) == 0 { - c.JSON(http.StatusNotFound, "Profile does not exist") + c.JSON(http.StatusNotFound, " "+profileName+" does not exist") return } @@ -2066,7 +2066,7 @@ func PostProfile(c *gin.Context) { return } if len(pf) != 0 { - c.JSON(http.StatusConflict, "Profile already exists") + c.JSON(http.StatusConflict, " "+profile.ProfileName+" already exists") return } @@ -2096,7 +2096,7 @@ func PutProfile(c *gin.Context) { return } if len(pf) == 0 { - c.JSON(http.StatusNotFound, "Profile does not exist") + c.JSON(http.StatusNotFound, " "+profileName+" does not exist") return } From 2249b7a2aacf70c53226323bd79eea343793bfb2 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 8 Nov 2024 13:57:41 +0000 Subject: [PATCH 25/28] feat: improve profile db operation error message behavior --- backend/WebUI/api_webui.go | 47 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/backend/WebUI/api_webui.go b/backend/WebUI/api_webui.go index 4c50ed04..4e2447d4 100644 --- a/backend/WebUI/api_webui.go +++ b/backend/WebUI/api_webui.go @@ -1968,11 +1968,14 @@ func DeleteProfile(c *gin.Context) { return } if len(pf) == 0 { - c.JSON(http.StatusNotFound, " "+profileName+" does not exist") + c.JSON(http.StatusNotFound, gin.H{"cause": profileName + " does not exist"}) return } - dbProfileOperation(profileName, "delete", nil) + if err = dbProfileOperation(profileName, "delete", nil); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) + return + } c.JSON(http.StatusOK, gin.H{"success": profileName + " has already been deleted"}) } @@ -2066,13 +2069,16 @@ func PostProfile(c *gin.Context) { return } if len(pf) != 0 { - c.JSON(http.StatusConflict, " "+profile.ProfileName+" already exists") + c.JSON(http.StatusConflict, gin.H{"cause": profile.ProfileName + " already exists"}) return } logger.ProcLog.Infof("PostProfile: %+v", profile.ProfileName) - dbProfileOperation(profile.ProfileName, "post", &profile) - c.JSON(http.StatusCreated, gin.H{}) + if err = dbProfileOperation(profile.ProfileName, "post", &profile); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) + return + } + c.JSON(http.StatusOK, profile) } // Put profile by profileName @@ -2096,7 +2102,7 @@ func PutProfile(c *gin.Context) { return } if len(pf) == 0 { - c.JSON(http.StatusNotFound, " "+profileName+" does not exist") + c.JSON(http.StatusNotFound, gin.H{"cause": profileName + " does not exist"}) return } @@ -2109,26 +2115,37 @@ func PutProfile(c *gin.Context) { profile.TenantId = tenantData["tenantId"].(string) logger.ProcLog.Infof("PutProfile: %+v", profile.ProfileName) - dbProfileOperation(profileName, "put", &profile) - c.JSON(http.StatusOK, gin.H{"success": profileName + " has already been updated"}) + if err = dbProfileOperation(profileName, "put", &profile); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) + return + } + c.JSON(http.StatusOK, profile) } -func dbProfileOperation(profileName string, method string, profile *Profile) { +func dbProfileOperation(profileName string, method string, profile *Profile) (err error) { + err = nil filter := bson.M{"profileName": profileName} // Replace all data with new one if method == "put" { - if err := mongoapi.RestfulAPIDeleteOne(profileDataColl, filter); err != nil { + if err = mongoapi.RestfulAPIDeleteOne(profileDataColl, filter); err != nil { logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) } } else if method == "delete" { - if err := mongoapi.RestfulAPIDeleteOne(profileListColl, filter); err != nil { + if err = mongoapi.RestfulAPIDeleteOne(profileListColl, filter); err != nil { logger.ProcLog.Errorf("DeleteSubscriberByID err: %+v", err) } - if err := mongoapi.RestfulAPIDeleteOne(profileDataColl, filter); err != nil { + if err = mongoapi.RestfulAPIDeleteOne(profileDataColl, filter); err != nil { logger.ProcLog.Errorf("DeleteSubscriberByID err: %+v", err) } } + + // Deal with error & early return + if err != nil { + return err + } + + // Insert data if method == "post" || method == "put" { profileListIE := ProfileListIE{ ProfileName: profileName, @@ -2136,11 +2153,13 @@ func dbProfileOperation(profileName string, method string, profile *Profile) { } profileListIEBsonM := toBsonM(profileListIE) profileBsonM := toBsonM(profile) - if _, err := mongoapi.RestfulAPIPost(profileListColl, filter, profileListIEBsonM); err != nil { + if _, err = mongoapi.RestfulAPIPost(profileListColl, filter, profileListIEBsonM); err != nil { logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) } - if _, err := mongoapi.RestfulAPIPost(profileDataColl, filter, profileBsonM); err != nil { + if _, err = mongoapi.RestfulAPIPost(profileDataColl, filter, profileBsonM); err != nil { logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) } } + + return err } From 91d6661ee26107abac94451945ed2e6b2939d993 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 8 Nov 2024 17:50:41 +0000 Subject: [PATCH 26/28] refactor: remove gpsi in profile --- backend/WebUI/api_webui.go | 31 +++---------- backend/WebUI/model_profile_list_ie.go | 6 --- frontend/src/lib/dtos/profile.ts | 6 +-- frontend/src/pages/ProfileList.tsx | 15 +++---- frontend/src/pages/SubscriberCreate/index.tsx | 6 +-- frontend/yarn.lock | 44 +++++++++++++++++++ 6 files changed, 62 insertions(+), 46 deletions(-) delete mode 100644 backend/WebUI/model_profile_list_ie.go diff --git a/backend/WebUI/api_webui.go b/backend/WebUI/api_webui.go index 4e2447d4..b2f00ce4 100644 --- a/backend/WebUI/api_webui.go +++ b/backend/WebUI/api_webui.go @@ -40,7 +40,6 @@ const ( userDataColl = "userData" tenantDataColl = "tenantData" identityDataColl = "subscriptionData.identityData" - profileListColl = "profileList" // store profile name and gpsi profileDataColl = "profileData" // store profile data ) @@ -1961,7 +1960,7 @@ func DeleteProfile(c *gin.Context) { logger.ProcLog.Infoln("Delete One Profile Data") profileName := c.Param("profileName") - pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profileName}) + pf, err := mongoapi.RestfulAPIGetOne(profileDataColl, bson.M{"profileName": profileName}) if err != nil { logger.ProcLog.Errorf("DeleteProfile err: %+v", err) c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) @@ -1991,8 +1990,8 @@ func GetProfiles(c *gin.Context) { return } - var pfList []ProfileListIE = make([]ProfileListIE, 0) - profileList, err := mongoapi.RestfulAPIGetMany(profileListColl, bson.M{}) + pfs := make([]string, 0) + profileList, err := mongoapi.RestfulAPIGetMany(profileDataColl, bson.M{}) if err != nil { logger.ProcLog.Errorf("GetProfiles err: %+v", err) c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) @@ -2000,15 +1999,10 @@ func GetProfiles(c *gin.Context) { } for _, profile := range profileList { profileName := profile["profileName"] - gpsi := profile["gpsi"] - tmp := ProfileListIE{ - ProfileName: profileName.(string), - Gpsi: gpsi.(string), - } - pfList = append(pfList, tmp) + pfs = append(pfs, profileName.(string)) } - c.JSON(http.StatusOK, pfList) + c.JSON(http.StatusOK, pfs) } // Get profile by profileName @@ -2062,7 +2056,7 @@ func PostProfile(c *gin.Context) { } profile.TenantId = tenantData["tenantId"].(string) - pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profile.ProfileName}) + pf, err := mongoapi.RestfulAPIGetOne(profileDataColl, bson.M{"profileName": profile.ProfileName}) if err != nil { logger.ProcLog.Errorf("GetProfile err: %+v", err) c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) @@ -2095,7 +2089,7 @@ func PutProfile(c *gin.Context) { return } - pf, err := mongoapi.RestfulAPIGetOne(profileListColl, bson.M{"profileName": profile.ProfileName}) + pf, err := mongoapi.RestfulAPIGetOne(profileDataColl, bson.M{"profileName": profile.ProfileName}) if err != nil { logger.ProcLog.Errorf("PutProfile err: %+v", err) c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) @@ -2132,9 +2126,6 @@ func dbProfileOperation(profileName string, method string, profile *Profile) (er logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) } } else if method == "delete" { - if err = mongoapi.RestfulAPIDeleteOne(profileListColl, filter); err != nil { - logger.ProcLog.Errorf("DeleteSubscriberByID err: %+v", err) - } if err = mongoapi.RestfulAPIDeleteOne(profileDataColl, filter); err != nil { logger.ProcLog.Errorf("DeleteSubscriberByID err: %+v", err) } @@ -2147,15 +2138,7 @@ func dbProfileOperation(profileName string, method string, profile *Profile) (er // Insert data if method == "post" || method == "put" { - profileListIE := ProfileListIE{ - ProfileName: profileName, - Gpsi: "", - } - profileListIEBsonM := toBsonM(profileListIE) profileBsonM := toBsonM(profile) - if _, err = mongoapi.RestfulAPIPost(profileListColl, filter, profileListIEBsonM); err != nil { - logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) - } if _, err = mongoapi.RestfulAPIPost(profileDataColl, filter, profileBsonM); err != nil { logger.ProcLog.Errorf("PutSubscriberByID err: %+v", err) } diff --git a/backend/WebUI/model_profile_list_ie.go b/backend/WebUI/model_profile_list_ie.go deleted file mode 100644 index 9c3c5043..00000000 --- a/backend/WebUI/model_profile_list_ie.go +++ /dev/null @@ -1,6 +0,0 @@ -package WebUI - -type ProfileListIE struct { - ProfileName string `json:"profileName"` - Gpsi string `json:"gpsi"` -} diff --git a/frontend/src/lib/dtos/profile.ts b/frontend/src/lib/dtos/profile.ts index aa15b7ba..ea6f72e4 100644 --- a/frontend/src/lib/dtos/profile.ts +++ b/frontend/src/lib/dtos/profile.ts @@ -102,14 +102,12 @@ export const snssaiConfigurationDTOSchema = z.object({ interface ProfileDTO { profileName: string; - gpsi?: string; subscribedUeAmbr: AmbrDTO; SnssaiConfigurations: SnssaiConfigurationDTO[]; } export const profileDTOSchema = z.object({ profileName: z.string(), - gpsi: z.string().optional(), subscribedUeAmbr: ambrDTOSchema, SnssaiConfigurations: z.array(snssaiConfigurationDTOSchema), }) @@ -224,7 +222,7 @@ class ProfileMapperImpl implements ProfileMapper { return { profileName: profile.profileName, AccessAndMobilitySubscriptionData: { - gpsis: [`msisdn-${profile.gpsi ?? ""}`], + gpsis: [`msisdn-`], subscribedUeAmbr: this.buildSubscriberAmbr(profile.subscribedUeAmbr), nssai: { defaultSingleNssais: profile.SnssaiConfigurations.filter((s) => s.isDefault).map( @@ -274,7 +272,6 @@ class ProfileMapperImpl implements ProfileMapper { mapFromProfile(profile: Profile): ProfileDTO { return { profileName: profile.profileName, - gpsi: profile.AccessAndMobilitySubscriptionData.gpsis?.[0]?.slice(7), subscribedUeAmbr: { uplink: profile.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.uplink ?? "", downlink: profile.AccessAndMobilitySubscriptionData.subscribedUeAmbr?.downlink ?? "", @@ -418,7 +415,6 @@ class ProfileMapperImpl implements ProfileMapper { export const defaultProfileDTO = (): ProfileDTO => ({ profileName: "profile-1", - gpsi: "", subscribedUeAmbr: { uplink: "1 Gbps", downlink: "2 Gbps", diff --git a/frontend/src/pages/ProfileList.tsx b/frontend/src/pages/ProfileList.tsx index e34b6293..738eaed1 100644 --- a/frontend/src/pages/ProfileList.tsx +++ b/frontend/src/pages/ProfileList.tsx @@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom"; import { config } from "../constants/config"; import Dashboard from "../Dashboard"; import axios from "../axios"; -import { Profile } from "../api/api"; import { Alert, Box, @@ -28,7 +27,7 @@ interface Props { function ProfileList(props: Props) { const navigation = useNavigate(); - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [limit, setLimit] = useState(50); const [page, setPage] = useState(0); const [searchTerm, setSearchTerm] = useState(""); @@ -110,12 +109,12 @@ function ProfileList(props: Props) { }); }; - const handleModify = (profile: Profile) => { - navigation("/profile/" + profile.profileName); + const handleModify = (profile: string) => { + navigation("/profile/" + profile); }; const filteredData = data.filter((profile) => - profile.profileName?.toLowerCase().includes(searchTerm.toLowerCase()), + profile.toLowerCase().includes(searchTerm.toLowerCase()), ); const handleSearch = (event: React.ChangeEvent) => { @@ -162,18 +161,18 @@ function ProfileList(props: Props) { {filteredData.map((row, index) => ( - {row.profileName} + {row.toString()} - diff --git a/frontend/src/pages/SubscriberCreate/index.tsx b/frontend/src/pages/SubscriberCreate/index.tsx index f1e52954..8f687e1d 100644 --- a/frontend/src/pages/SubscriberCreate/index.tsx +++ b/frontend/src/pages/SubscriberCreate/index.tsx @@ -33,7 +33,7 @@ function SubscriberCreate() { const isNewSubscriber = id === undefined && plmn === undefined; const navigation = useNavigate(); const [loading, setLoading] = useState(false); - const [profiles, setProfiles] = useState([]); + const [profiles, setProfiles] = useState([]); const [selectedProfile, setSelectedProfile] = useState(''); const { handleSubmit, getValues, reset } = useSubscriptionForm(); @@ -211,8 +211,8 @@ function SubscriberCreate() { onChange={handleProfileChange} > {profiles.map((profile) => ( - - {profile.profileName} + + {profile} ))} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 72d10b87..14cd9cd8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3767,6 +3767,7 @@ __metadata: react-hook-form: "npm:^7.51.5" react-router-dom: "npm:^6.22.2" typescript: "npm:^5.3.3" + vite: "npm:^5.2.14" vitest: "npm:^2.0.2" web-vitals: "npm:^3.5.2" zod: "npm:^3.23.8" @@ -5094,6 +5095,49 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.2.14": + version: 5.4.10 + resolution: "vite@npm:5.4.10" + dependencies: + esbuild: "npm:^0.21.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.43" + rollup: "npm:^4.20.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/4ef4807d2fd166a920de244dbcec791ba8a903b017a7d8e9f9b4ac40d23f8152c1100610583d08f542b47ca617a0505cfc5f8407377d610599d58296996691ed + languageName: node + linkType: hard + "vitest@npm:^2.0.2": version: 2.0.2 resolution: "vitest@npm:2.0.2" From 8423dd7f675880a3f694da59eb900dee84428f56 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 8 Nov 2024 17:52:38 +0000 Subject: [PATCH 27/28] fix: solve 404 error when refresh profile page --- backend/webui_service/middleware.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/webui_service/middleware.go b/backend/webui_service/middleware.go index 864569fd..343018c4 100644 --- a/backend/webui_service/middleware.go +++ b/backend/webui_service/middleware.go @@ -30,6 +30,7 @@ func verifyDestPath(requestedURI string) string { "status", "analysis", "subscriber", + "profile", "tenant", "charging", "login", From cbc47fc8c959bac77d36bc47c1c746c18ed26c2e Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Mon, 11 Nov 2024 14:35:01 +0000 Subject: [PATCH 28/28] fix: remove gpsi in profile dto --- frontend/src/lib/dtos/profile.test.ts | 1 - frontend/src/lib/dtos/profile.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/lib/dtos/profile.test.ts b/frontend/src/lib/dtos/profile.test.ts index c278b761..f1fd2ed4 100644 --- a/frontend/src/lib/dtos/profile.test.ts +++ b/frontend/src/lib/dtos/profile.test.ts @@ -15,7 +15,6 @@ import assert from "node:assert"; const defaultProfile = (): Profile => ({ profileName: "profile-1", AccessAndMobilitySubscriptionData: { - gpsis: ["msisdn-"], subscribedUeAmbr: { uplink: "1 Gbps", downlink: "2 Gbps", diff --git a/frontend/src/lib/dtos/profile.ts b/frontend/src/lib/dtos/profile.ts index ea6f72e4..2817f96e 100644 --- a/frontend/src/lib/dtos/profile.ts +++ b/frontend/src/lib/dtos/profile.ts @@ -222,7 +222,6 @@ class ProfileMapperImpl implements ProfileMapper { return { profileName: profile.profileName, AccessAndMobilitySubscriptionData: { - gpsis: [`msisdn-`], subscribedUeAmbr: this.buildSubscriberAmbr(profile.subscribedUeAmbr), nssai: { defaultSingleNssais: profile.SnssaiConfigurations.filter((s) => s.isDefault).map(