diff --git a/API/models/model.go b/API/models/model.go index 80d1d4c8..53f35cf2 100644 --- a/API/models/model.go +++ b/API/models/model.go @@ -90,14 +90,18 @@ func updateOldObjWithPatch(old map[string]interface{}, patch map[string]interfac for k, v := range patch { switch patchValueCasted := v.(type) { case map[string]interface{}: - switch oldValueCasted := old[k].(type) { - case map[string]interface{}: - err := updateOldObjWithPatch(oldValueCasted, patchValueCasted) - if err != nil { - return err + if k == "pillars" || k == "separators" { + old[k] = v + } else { + switch oldValueCasted := old[k].(type) { + case map[string]interface{}: + err := updateOldObjWithPatch(oldValueCasted, patchValueCasted) + if err != nil { + return err + } + default: + old[k] = v } - default: - return errors.New("Wrong format for property " + k) } default: if k == "filter" && strings.HasPrefix(v.(string), "&") { diff --git a/APP/lib/l10n/app_en.arb b/APP/lib/l10n/app_en.arb index 3f3e639e..c418e61f 100644 --- a/APP/lib/l10n/app_en.arb +++ b/APP/lib/l10n/app_en.arb @@ -215,5 +215,7 @@ "viewAlerts": "View alerts", "temperatureAlert1": "The temperature of the device ", "temperatureAlert2": " is higher than usual. This could impact performance for your applications: \"my-frontend-app\" and \"my-backend-app\".", - "oneAlert": "1 minor alert" + "oneAlert": "1 minor alert", + + "virtualConfigTitle" : "Virtual Configuration:" } \ No newline at end of file diff --git a/APP/lib/l10n/app_es.arb b/APP/lib/l10n/app_es.arb index f076ea09..913ffbfc 100644 --- a/APP/lib/l10n/app_es.arb +++ b/APP/lib/l10n/app_es.arb @@ -215,5 +215,7 @@ "viewAlerts": "Ver alertas", "temperatureAlert1": "La temperatura del dispositivo ", "temperatureAlert2": " es más alta de lo usual. Esto podría impactar el rendimiento de sus aplicaciones: \"my-frontend-app\" y \"my-backend-app\".", - "oneAlert": "1 alerta menor" + "oneAlert": "1 alerta menor", + + "virtualConfigTitle" : "Virtual Config:" } \ No newline at end of file diff --git a/APP/lib/l10n/app_fr.arb b/APP/lib/l10n/app_fr.arb index cf632710..4537ca76 100644 --- a/APP/lib/l10n/app_fr.arb +++ b/APP/lib/l10n/app_fr.arb @@ -283,6 +283,8 @@ "viewAlerts": "Voir les alertes", "temperatureAlert1": "La temperature du device ", "temperatureAlert2": " est plus élevée que d'habitude. Cela peut avoir un impact sur les performances de vos applications : \"my-frontend-app\" et \"my-backend-app\".", - "oneAlert": "1 alerte mineur" + "oneAlert": "1 alerte mineur", + + "virtualConfigTitle" : "Virtual Config :" } \ No newline at end of file diff --git a/APP/lib/l10n/app_pt.arb b/APP/lib/l10n/app_pt.arb index 3a2ca405..f87b4a59 100644 --- a/APP/lib/l10n/app_pt.arb +++ b/APP/lib/l10n/app_pt.arb @@ -215,5 +215,7 @@ "viewAlerts": "Ver alertas", "temperatureAlert1": "A temperature do device ", "temperatureAlert2": " está acima do normal. Isso pode impactar a performance de suas aplicações: \"my-frontend-app\" e \"my-backend-app\"", - "oneAlert": "1 alerta menor" + "oneAlert": "1 alerta menor", + + "virtualConfigTitle" : "Virtual Config:" } \ No newline at end of file diff --git a/APP/lib/widgets/object_graph_view.dart b/APP/lib/widgets/object_graph_view.dart index 05d3797b..7e1cc54c 100644 --- a/APP/lib/widgets/object_graph_view.dart +++ b/APP/lib/widgets/object_graph_view.dart @@ -143,8 +143,8 @@ class _ObjectGraphViewState extends State { final node = Node.Id(value["id"]); graph.addNode(node); idCategory[value["id"]] = value["category"]; - if (value["attributes"] != null && value["attributes"]["vlink"] != null) { - for (var vlink in List.from(value["attributes"]["vlink"])) { + if (value["attributes"] != null && value["attributes"]["vlinks"] != null) { + for (var vlink in List.from(value["attributes"]["vlinks"])) { graph.addEdge(node, Node.Id(vlink), paint: Paint()..color = Colors.purple); } diff --git a/APP/lib/widgets/select_objects/object_popup.dart b/APP/lib/widgets/select_objects/object_popup.dart index deaff865..15005a3b 100644 --- a/APP/lib/widgets/select_objects/object_popup.dart +++ b/APP/lib/widgets/select_objects/object_popup.dart @@ -303,7 +303,10 @@ class _ObjectPopupState extends State { case 3: categories = [PhyCategories.device.name, PhyCategories.group.name]; default: - categories = [PhyCategories.device.name]; + categories = [ + PhyCategories.device.name, + PhyCategories.virtual_obj.name + ]; } } return categories.map>((String value) { @@ -398,6 +401,7 @@ class _ObjectPopupState extends State { var attrs = Map.from( jsonResult["properties"]["attributes"]["properties"]); categoryAttrs[obj] = attrs.keys.toList(); + categoryAttrs[obj]!.remove("virtual_config"); if (jsonResult["properties"]["attributes"]["required"] != null) { // Get required ones var requiredAttrs = List.from( @@ -526,7 +530,8 @@ class _ObjectPopupState extends State { for (var attr in objDataAttrs.entries) { if (!categoryAttrs[_objCategory]!.contains(attr.key) && !categoryAttrs[_objCategory]! - .contains(starSymbol + attr.key)) { + .contains(starSymbol + attr.key) && + attr.key != "virtual_config") { // add custom attribute customAttributesRows.add(CustomAttrRow( customAttributesRows.length, @@ -545,7 +550,8 @@ class _ObjectPopupState extends State { for (var attr in objDataAttrs.entries) { if (!categoryAttrs[_objCategory]!.contains(attr.key) && !categoryAttrs[_objCategory]! - .contains(starSymbol + attr.key)) { + .contains(starSymbol + attr.key) && + attr.key != "virtual_config") { // add custom attribute customAttributesRows.add(CustomAttrRow( customAttributesRows.length, @@ -807,7 +813,9 @@ class _ObjectPopupState extends State { if (newValue != null && newValue.isNotEmpty) { // check type var numValue = num.tryParse(newValue); - if (numValue != null) { + if (attrKey == "virtual_config") { + objDataAttrs[attrKey] = json.decode(newValue); + } else if (numValue != null) { // is number objDataAttrs[attrKey] = numValue.toDouble(); } else if (newValue.length >= 2 && @@ -870,6 +878,57 @@ class _ObjectPopupState extends State { label: Text(localeMsg.attribute)), ), ), + virtualConfigInput(), + ], + ); + } + + virtualConfigInput() { + if (_objCategory != "virtual_obj" && _objCategory != "device") { + return Container(); + } + Map virtualAttrs = { + "clusterId": "string (e.g. kube-cluster)", + "type": "string (e.g. node)", + "role": "string (e.g. master)" + }; + List virtualAttrsKeys = virtualAttrs.keys.toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8.0, left: 6, bottom: 6), + child: Text(AppLocalizations.of(context)!.virtualConfigTitle), + ), + SizedBox( + height: + (virtualAttrsKeys.length ~/ 2 + virtualAttrsKeys.length % 2) * 60, + child: GridView.count( + physics: const NeverScrollableScrollPhysics(), + childAspectRatio: 3.5, + shrinkWrap: true, + padding: const EdgeInsets.only(left: 4), + crossAxisCount: 2, + children: List.generate(virtualAttrsKeys.length, (index) { + return CustomFormField( + tipStr: virtualAttrs[virtualAttrsKeys[index]] ?? "string", + save: (newValue) { + if (objDataAttrs["virtual_config"] == null) { + objDataAttrs["virtual_config"] = {}; + } + objDataAttrs["virtual_config"][virtualAttrsKeys[index]] = + newValue; + }, + label: virtualAttrsKeys[index], + icon: Icons.tag_sharp, + isCompact: true, + shouldValidate: false, + initialValue: objDataAttrs["virtual_config"] + ?[virtualAttrsKeys[index]] + ?.toString()); + }), + ), + ), ], ); } diff --git a/APP/lib/widgets/select_objects/view_object_popup.dart b/APP/lib/widgets/select_objects/view_object_popup.dart index db6d995a..a8e75cda 100644 --- a/APP/lib/widgets/select_objects/view_object_popup.dart +++ b/APP/lib/widgets/select_objects/view_object_popup.dart @@ -110,16 +110,15 @@ class _ViewObjectPopupState extends State { } List> getCategoryMenuItems() { - List categories = objsByNamespace[widget.namespace]!; - return categories.map>((String value) { - return DropdownMenuItem( - value: value, + return [ + DropdownMenuItem( + value: _objCategory, child: Text( - value, + _objCategory, overflow: TextOverflow.ellipsis, ), - ); - }).toList(); + ) + ]; } getObject() async { diff --git a/CLI/ast.go b/CLI/ast.go index 6ee02567..36c14319 100644 --- a/CLI/ast.go +++ b/CLI/ast.go @@ -546,15 +546,14 @@ func setLabelBackground(path string, values []any) (map[string]any, error) { return nil, cmd.C.InteractObject(path, "labelBackground", c, false) } -func addToStringMap[T any](stringMap string, key string, val T) (string, bool) { - m := map[string]T{} - if stringMap != "" { - json.Unmarshal([]byte(stringMap), &m) +func addToMap[T any](mapToAdd any, key string, val T) (map[string]any, bool) { + attrMap, ok := mapToAdd.(map[string]any) + if !ok { + attrMap = map[string]any{} } - _, keyExist := m[key] - m[key] = val - mBytes, _ := json.Marshal(m) - return string(mBytes), keyExist + _, keyExist := attrMap[key] + attrMap[key] = val + return attrMap, keyExist } func removeFromStringMap[T any](stringMap string, key string) (string, bool) { @@ -602,10 +601,9 @@ func addRoomSeparator(path string, values []any) (map[string]any, error) { return nil, err } attr := obj["attributes"].(map[string]any) - separators, _ := attr["separators"].(string) newSeparator := Separator{startPos, endPos, sepType} var keyExist bool - attr["separators"], keyExist = addToStringMap[Separator](separators, name, newSeparator) + attr["separators"], keyExist = addToMap[Separator](attr["separators"], name, newSeparator) obj, err = cmd.C.UpdateObj(path, map[string]any{"attributes": attr}, false) if err != nil { return nil, err @@ -647,10 +645,9 @@ func addRoomPillar(path string, values []any) (map[string]any, error) { return nil, err } attr := obj["attributes"].(map[string]any) - pillars, _ := attr["pillars"].(string) newPillar := Pillar{centerXY, sizeXY, rotation} var keyExist bool - attr["pillars"], keyExist = addToStringMap[Pillar](pillars, name, newPillar) + attr["pillars"], keyExist = addToMap[Pillar](attr["pillars"], name, newPillar) obj, err = cmd.C.UpdateObj(path, map[string]any{"attributes": attr}, false) if err != nil { return nil, err @@ -668,21 +665,12 @@ func deleteRoomPillarOrSeparator(path, attribute, name string) (map[string]any, return nil, err } attributes := obj["attributes"].(map[string]any) - stringMap, _ := attributes[attribute+"s"].(string) - var ok bool - switch attribute { - case "pillar": - stringMap, ok = removeFromStringMap[Pillar](stringMap, name) - case "separator": - stringMap, ok = removeFromStringMap[Separator](stringMap, name) - default: - return nil, errors.New("\"separator\" or \"pillar\" expected") - } - - if !ok { + attrMap, ok := attributes[attribute+"s"].(map[string]any) + if !ok || attrMap[name] == nil { return nil, fmt.Errorf("%s %s does not exist", attribute, name) } - attributes[attribute+"s"] = stringMap + delete(attrMap, name) + attributes[attribute+"s"] = attrMap return cmd.C.UpdateObj(path, map[string]any{"attributes": attributes}, false) } diff --git a/CLI/ast_test.go b/CLI/ast_test.go index f03c47aa..7a43d3eb 100644 --- a/CLI/ast_test.go +++ b/CLI/ast_test.go @@ -387,14 +387,14 @@ func TestSetLabel(t *testing.T) { assert.Nil(t, value) } -func TestAddToStringMap(t *testing.T) { - newMap, replaced := addToStringMap[int]("{\"a\":3}", "b", 10) +func TestAddToMap(t *testing.T) { + newMap, replaced := addToMap[int](map[string]any{"a": 3}, "b", 10) - assert.Equal(t, "{\"a\":3,\"b\":10}", newMap) + assert.Equal(t, map[string]any{"a": 3, "b": 10}, newMap) assert.False(t, replaced) - newMap, replaced = addToStringMap[int](newMap, "b", 15) - assert.Equal(t, "{\"a\":3,\"b\":15}", newMap) + newMap, replaced = addToMap[int](newMap, "b", 15) + assert.Equal(t, map[string]any{"a": 3, "b": 15}, newMap) assert.True(t, replaced) } @@ -438,8 +438,8 @@ func TestAddRoomSeparatorOrPillarWorks(t *testing.T) { values []any newAttributes map[string]any }{ - {"AddRoomSeparator", addRoomSeparator, []any{"mySeparator", []float64{1., 2.}, []float64{1., 2.}, "wireframe"}, map[string]interface{}{"separators": "{\"mySeparator\":{\"startPosXYm\":[1,2],\"endPosXYm\":[1,2],\"type\":\"wireframe\"}}"}}, - {"AddRoomPillar", addRoomPillar, []any{"myPillar", []float64{1., 2.}, []float64{1., 2.}, 2.5}, map[string]interface{}{"pillars": "{\"myPillar\":{\"centerXY\":[1,2],\"sizeXY\":[1,2],\"rotation\":2.5}}"}}, + {"AddRoomSeparator", addRoomSeparator, []any{"mySeparator", []float64{1., 2.}, []float64{1., 2.}, "wireframe"}, map[string]interface{}{"separators": map[string]interface{}{"mySeparator": Separator{StartPos: []float64{1, 2}, EndPos: []float64{1, 2}, Type: "wireframe"}}}}, + {"AddRoomPillar", addRoomPillar, []any{"myPillar", []float64{1., 2.}, []float64{1., 2.}, 2.5}, map[string]interface{}{"pillars": map[string]interface{}{"myPillar": Pillar{CenterXY: []float64{1, 2}, SizeXY: []float64{1, 2}, Rotation: 2.5}}}}, } for _, tt := range tests { @@ -467,7 +467,7 @@ func TestDeleteRoomPillarOrSeparatorWithError(t *testing.T) { separatorName string errorMessage string }{ - {"InvalidArgument", "other", "separator", "\"separator\" or \"pillar\" expected"}, + {"InvalidArgument", "other", "separator", "other separator does not exist"}, {"SeparatorDoesNotExist", "separator", "mySeparator", "separator mySeparator does not exist"}, } @@ -490,15 +490,15 @@ func TestDeleteRoomPillarOrSeparatorSeparator(t *testing.T) { _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) room := test_utils.GetEntity("room", "room", "site.building", "domain") - room["attributes"].(map[string]any)["separators"] = "{\"mySeparator\":{\"startPosXYm\":[1,2],\"endPosXYm\":[1,2],\"type\":\"wireframe\"}}" + room["attributes"].(map[string]any)["separators"] = map[string]interface{}{"mySeparator": Separator{StartPos: []float64{1, 2}, EndPos: []float64{1, 2}, Type: "wireframe"}} updatedRoom := test_utils.GetEntity("room", "room", "site.building", "domain") - updatedRoom["attributes"] = map[string]any{"separators": "{}"} + updatedRoom["attributes"] = map[string]any{"separators": map[string]interface{}{}} test_utils.MockGetObject(mockAPI, room) test_utils.MockGetObject(mockAPI, room) - test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"separators": "{}"}}, updatedRoom) + test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"separators": map[string]interface{}{}}}, updatedRoom) obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "separator", "mySeparator") assert.Nil(t, err) @@ -509,14 +509,14 @@ func TestDeleteRoomPillarOrSeparatorPillar(t *testing.T) { _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) room := test_utils.GetEntity("room", "room", "site.building", "domain") - room["attributes"].(map[string]any)["pillars"] = "{\"myPillar\":{\"centerXY\":[1,2],\"sizeXY\":[1,2],\"rotation\":\"2.5\"}}" + room["attributes"].(map[string]any)["pillars"] = map[string]interface{}{"myPillar": Pillar{CenterXY: []float64{1, 2}, SizeXY: []float64{1, 2}, Rotation: 2.5}} updatedRoom := maps.Clone(room) - updatedRoom["attributes"] = map[string]any{"pillars": "{}"} + updatedRoom["attributes"] = map[string]any{"pillars": map[string]interface{}{}} test_utils.MockGetObject(mockAPI, room) test_utils.MockGetObject(mockAPI, room) - test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"pillars": "{}"}}, updatedRoom) + test_utils.MockUpdateObject(mockAPI, map[string]interface{}{"attributes": map[string]interface{}{"pillars": map[string]interface{}{}}}, updatedRoom) obj, err := deleteRoomPillarOrSeparator("/Physical/site/building/room", "pillar", "myPillar") assert.Nil(t, err)