From bf9cffcdcb39a07cb8a8a6e57b153a4b1efbf2b6 Mon Sep 17 00:00:00 2001 From: helderbetiol <37706737+helderbetiol@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:21:37 +0200 Subject: [PATCH] feat(app) add graph view --- APP/lib/common/appbar.dart | 2 +- APP/lib/pages/alert_page.dart | 46 +++-- APP/lib/pages/login_page.dart | 4 +- .../{ => login}/change_password_popup.dart | 4 +- APP/lib/widgets/{ => login}/login_card.dart | 0 APP/lib/widgets/{ => login}/reset_card.dart | 4 +- APP/lib/widgets/object_graph_view.dart | 160 ++++++++++++++++++ .../tree_view/tree_node_tile.dart | 2 +- .../select_objects/view_object_popup.dart | 110 ++++++------ APP/pubspec.lock | 8 + APP/pubspec.yaml | 1 + 11 files changed, 248 insertions(+), 93 deletions(-) rename APP/lib/widgets/{ => login}/change_password_popup.dart (98%) rename APP/lib/widgets/{ => login}/login_card.dart (100%) rename APP/lib/widgets/{ => login}/reset_card.dart (98%) create mode 100644 APP/lib/widgets/object_graph_view.dart diff --git a/APP/lib/common/appbar.dart b/APP/lib/common/appbar.dart index 9e0c20d68..28cad412c 100644 --- a/APP/lib/common/appbar.dart +++ b/APP/lib/common/appbar.dart @@ -5,7 +5,7 @@ import 'package:ogree_app/models/netbox.dart'; import 'package:ogree_app/pages/login_page.dart'; import 'package:ogree_app/pages/projects_page.dart'; import 'package:ogree_app/pages/tenant_page.dart'; -import 'package:ogree_app/widgets/change_password_popup.dart'; +import 'package:ogree_app/widgets/login/change_password_popup.dart'; import 'package:ogree_app/widgets/common/language_toggle.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:ogree_app/widgets/tools/download_tool_popup.dart'; diff --git a/APP/lib/pages/alert_page.dart b/APP/lib/pages/alert_page.dart index e877f3254..e18d73e55 100644 --- a/APP/lib/pages/alert_page.dart +++ b/APP/lib/pages/alert_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:ogree_app/common/appbar.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:ogree_app/models/tenant.dart'; import 'package:ogree_app/pages/projects_page.dart'; import 'package:ogree_app/widgets/select_objects/treeapp_controller.dart'; @@ -25,7 +24,6 @@ class AlertPageState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final localeMsg = AppLocalizations.of(context)!; return Scaffold( backgroundColor: const Color.fromARGB(255, 238, 238, 241), appBar: myAppBar(context, widget.userEmail, @@ -90,7 +88,7 @@ class AlertPageState extends State with TickerProviderStateMixin { ], ), ), - VerticalDivider( + const VerticalDivider( width: 30, thickness: 0.5, color: Colors.grey, @@ -141,7 +139,7 @@ class AlertPageState extends State with TickerProviderStateMixin { backgroundColor: typeColor, label: Text( " $type ", - style: TextStyle( + style: const TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.black), @@ -167,7 +165,7 @@ class AlertPageState extends State with TickerProviderStateMixin { TextSpan( children: [ TextSpan( - style: new TextStyle( + style: const TextStyle( fontSize: 14.0, color: Colors.black, ), @@ -176,31 +174,29 @@ class AlertPageState extends State with TickerProviderStateMixin { text: 'Minor Alert\n', style: Theme.of(context).textTheme.headlineLarge, ), - TextSpan(text: '\nThe temperature of device '), - TextSpan( + const TextSpan(text: '\nThe temperature of device '), + const TextSpan( text: 'BASIC.A.R1.A02.chassis01', - style: new TextStyle(fontWeight: FontWeight.bold)), - TextSpan( + style: TextStyle(fontWeight: FontWeight.bold)), + const TextSpan( text: ' is higher than usual. This could impact the performance of your applications running in a Kubernetes cluster with nodes in this device: "my-frontend-app" and "my-backend-app".\n'), ...getSubtitle("Details:"), - TextSpan( + const TextSpan( text: 'The last measurement of temperature for the device in question reads '), - TextSpan( - text: '64°C', - style: new TextStyle(fontWeight: FontWeight.bold)), - TextSpan( + const TextSpan( + text: '64°C', style: TextStyle(fontWeight: FontWeight.bold)), + const TextSpan( text: '. The temperature recommendation for a chassis of this type is to not surpass '), - TextSpan( - text: '55°C', - style: new TextStyle(fontWeight: FontWeight.bold)), - TextSpan(text: '.\n'), + const TextSpan( + text: '55°C', style: TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: '.\n'), ...getSubtitle("Impacted by this alert:"), getWidgetSpan(" BASIC.A.R1.A02.chassis01", "Physical - Device", Colors.teal), - TextSpan(text: '\n'), + const TextSpan(text: '\n'), ...getSubtitle("May also be impacted:"), getWidgetSpan(" BASIC.A.R1.A02.chassis01.blade01", "Physical - Device", Colors.teal), @@ -224,7 +220,7 @@ class AlertPageState extends State with TickerProviderStateMixin { TextSpan( children: [ TextSpan( - style: new TextStyle( + style: const TextStyle( fontSize: 14.0, color: Colors.black, ), @@ -233,15 +229,15 @@ class AlertPageState extends State with TickerProviderStateMixin { text: 'Hint\n', style: Theme.of(context).textTheme.headlineLarge, ), - TextSpan( + const TextSpan( text: '\nAll nodes of a kubernetes cluster are servers from the same rack.\n'), ...getSubtitle("Details:"), - TextSpan( + const TextSpan( text: 'The Kubernetes cluster "kubernetes-cluster" has the following devices as its nodes: "chassis01.blade01" , "chassis01.blade02" and "chassis01.blade03". All of these devices are in the same rack "BASIC.A.R1.A02".\n'), ...getSubtitle("Suggestion:"), - TextSpan( + const TextSpan( text: 'To limit impacts to the cluster and its applications in case of issue with this rack, consider adding a server from a different rack as a node to this cluster.\n'), ...getSubtitle("Impacted by this hint:"), @@ -249,7 +245,7 @@ class AlertPageState extends State with TickerProviderStateMixin { " BASIC.A.R1.A02", "Physical - Device", Colors.teal), getWidgetSpan(" kubernetes-cluster", "Logical - Application", Colors.deepPurple), - TextSpan(text: '\n'), + const TextSpan(text: '\n'), ...getSubtitle("May also be impacted:"), getWidgetSpan(" kubernetes-cluster.my-frontend-app", "Logical - Application", Colors.deepPurple), @@ -295,7 +291,7 @@ class AlertPageState extends State with TickerProviderStateMixin { text: '\n$subtitle\n', style: Theme.of(context).textTheme.headlineMedium, ), - TextSpan(text: '\n'), + const TextSpan(text: '\n'), ]; } } diff --git a/APP/lib/pages/login_page.dart b/APP/lib/pages/login_page.dart index ea44e9f8b..a0908dd19 100644 --- a/APP/lib/pages/login_page.dart +++ b/APP/lib/pages/login_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ogree_app/widgets/common/language_toggle.dart'; -import 'package:ogree_app/widgets/login_card.dart'; -import 'package:ogree_app/widgets/reset_card.dart'; +import 'package:ogree_app/widgets/login/login_card.dart'; +import 'package:ogree_app/widgets/login/reset_card.dart'; class LoginPage extends StatefulWidget { final bool isPasswordReset; diff --git a/APP/lib/widgets/change_password_popup.dart b/APP/lib/widgets/login/change_password_popup.dart similarity index 98% rename from APP/lib/widgets/change_password_popup.dart rename to APP/lib/widgets/login/change_password_popup.dart index 16303350a..e626d453e 100644 --- a/APP/lib/widgets/change_password_popup.dart +++ b/APP/lib/widgets/login/change_password_popup.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:ogree_app/common/api_backend.dart'; import 'package:ogree_app/common/definitions.dart'; +import 'package:ogree_app/common/snackbar.dart'; import 'package:ogree_app/common/theme.dart'; import 'package:ogree_app/widgets/common/form_field.dart'; -import '../common/snackbar.dart'; - class ChangePasswordPopup extends StatefulWidget { const ChangePasswordPopup({super.key}); diff --git a/APP/lib/widgets/login_card.dart b/APP/lib/widgets/login/login_card.dart similarity index 100% rename from APP/lib/widgets/login_card.dart rename to APP/lib/widgets/login/login_card.dart diff --git a/APP/lib/widgets/reset_card.dart b/APP/lib/widgets/login/reset_card.dart similarity index 98% rename from APP/lib/widgets/reset_card.dart rename to APP/lib/widgets/login/reset_card.dart index 097ef13f8..6499132a0 100644 --- a/APP/lib/widgets/reset_card.dart +++ b/APP/lib/widgets/login/reset_card.dart @@ -6,7 +6,7 @@ import 'package:ogree_app/common/snackbar.dart'; import 'package:ogree_app/common/theme.dart'; import 'package:ogree_app/pages/login_page.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:ogree_app/widgets/login_card.dart'; +import 'package:ogree_app/widgets/login/login_card.dart'; class ResetCard extends StatelessWidget { final _formKey = GlobalKey(); @@ -57,7 +57,7 @@ class ResetCard extends StatelessWidget { ) : Center( child: Image.asset( - "assets/edf_logo.png", + "assets/custom/logo.png", height: 30, ), ), diff --git a/APP/lib/widgets/object_graph_view.dart b/APP/lib/widgets/object_graph_view.dart new file mode 100644 index 000000000..05d3797b7 --- /dev/null +++ b/APP/lib/widgets/object_graph_view.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:graphview/GraphView.dart'; +import 'package:ogree_app/common/api_backend.dart'; +import 'package:ogree_app/common/definitions.dart'; +import 'package:ogree_app/common/snackbar.dart'; +import 'package:ogree_app/common/theme.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ObjectGraphView extends StatefulWidget { + String rootId; + ObjectGraphView(this.rootId); + @override + _ObjectGraphViewState createState() => _ObjectGraphViewState(); +} + +class _ObjectGraphViewState extends State { + bool loaded = false; + Map idCategory = {}; + + final Graph graph = Graph(); + + SugiyamaConfiguration builder = SugiyamaConfiguration() + ..bendPointShape = CurvedBendPointShape(curveLength: 10); + + @override + void initState() { + super.initState(); + + builder + ..nodeSeparation = (25) + ..levelSeparation = (35) + ..orientation = SugiyamaConfiguration.ORIENTATION_TOP_BOTTOM; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: !loaded ? getObject() : null, + builder: (context, _) { + if (!loaded) { + return const Center(child: CircularProgressIndicator()); + } + return Center( + child: Container( + constraints: const BoxConstraints(maxHeight: 520, maxWidth: 800), + margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + decoration: PopupDecoration, + child: Padding( + padding: const EdgeInsets.fromLTRB(40, 20, 40, 15), + child: Scaffold( + backgroundColor: Colors.white, + body: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Center( + child: Text( + AppLocalizations.of(context)!.viewGraph, + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + Expanded( + child: InteractiveViewer( + alignment: Alignment.center, + constrained: true, + boundaryMargin: + const EdgeInsets.all(double.infinity), + minScale: 0.0001, + maxScale: 10.6, + child: OverflowBox( + alignment: Alignment.center, + minWidth: 0.0, + minHeight: 0.0, + maxWidth: double.infinity, + maxHeight: double.infinity, + child: GraphView( + graph: graph, + algorithm: SugiyamaAlgorithm(builder), + paint: Paint() + ..color = Colors.blue + ..strokeWidth = 1 + ..style = PaintingStyle.stroke, + builder: (Node node) { + var a = node.key!.value as String?; + return rectangleWidget(a!); + }, + ), + )), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + }, + label: const Text("OK"), + icon: const Icon(Icons.thumb_up, size: 16)) + ], + ) + ], + ), + ), + ), + ), + ); + }); + } + + Widget rectangleWidget(String a) { + return Tooltip( + message: a, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: idCategory[a] == "virtual_obj" + ? Colors.purple[100]! + : Colors.blue[100]!, + spreadRadius: 1), + ], + ), + child: Text('${a.split(".").last}')), + ); + } + + getObject() async { + final messenger = ScaffoldMessenger.of(context); + var result = await fetchObjectChildren(widget.rootId); + switch (result) { + case Success(value: final value): + print(value); + addToGraph(value); + loaded = true; + return; + case Failure(exception: final exception): + showSnackBar(messenger, exception.toString(), isError: true); + } + } + + addToGraph(Map value) { + 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"])) { + graph.addEdge(node, Node.Id(vlink), + paint: Paint()..color = Colors.purple); + } + } + if (value["children"] != null) { + for (var child in List>.from(value["children"])) { + var childNode = addToGraph(child); + graph.addEdge(node, childNode); + } + } + return node; + } +} diff --git a/APP/lib/widgets/select_objects/tree_view/tree_node_tile.dart b/APP/lib/widgets/select_objects/tree_view/tree_node_tile.dart index 215aad437..b14ff854d 100644 --- a/APP/lib/widgets/select_objects/tree_view/tree_node_tile.dart +++ b/APP/lib/widgets/select_objects/tree_view/tree_node_tile.dart @@ -5,7 +5,7 @@ import 'package:ogree_app/common/definitions.dart'; import 'package:ogree_app/common/popup_dialog.dart'; import 'package:ogree_app/common/snackbar.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:ogree_app/pages/layer_graph.dart'; +import 'package:ogree_app/widgets/object_graph_view.dart'; import 'package:ogree_app/pages/tenant_page.dart'; import 'package:ogree_app/widgets/select_objects/view_object_popup.dart'; import 'package:ogree_app/widgets/select_objects/object_popup.dart'; diff --git a/APP/lib/widgets/select_objects/view_object_popup.dart b/APP/lib/widgets/select_objects/view_object_popup.dart index a2226dbea..01a0e55f7 100644 --- a/APP/lib/widgets/select_objects/view_object_popup.dart +++ b/APP/lib/widgets/select_objects/view_object_popup.dart @@ -19,7 +19,6 @@ class ViewObjectPopup extends StatefulWidget { } class _ViewObjectPopupState extends State { - final _formKey = GlobalKey(); bool _isSmallDisplay = false; String _objCategory = LogCategories.group.name; String? _loadFileResult; @@ -49,68 +48,61 @@ class _ViewObjectPopupState extends State { decoration: PopupDecoration, child: Padding( padding: const EdgeInsets.fromLTRB(40, 20, 40, 15), - child: Form( - key: _formKey, - child: ScaffoldMessenger( - child: Builder( - builder: (context) => Scaffold( - backgroundColor: Colors.white, - body: SingleChildScrollView( - child: Column( + child: ScaffoldMessenger( + child: Builder( + builder: (context) => Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Column( + children: [ + Center( + child: Text( + localeMsg.viewJSON, + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Center( - child: Text( - localeMsg.viewJSON, - style: Theme.of(context) - .textTheme - .headlineMedium, - ), - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(localeMsg.objType), - const SizedBox(width: 20), - SizedBox( - height: 35, - width: 147, - child: DropdownButtonFormField( - isExpanded: true, - borderRadius: - BorderRadius.circular(12.0), - decoration: GetFormInputDecoration( - false, - null, - icon: Icons.bookmark, - ), - value: _objCategory, - items: getCategoryMenuItems(), - onChanged: null), - ), - ], - ), - const SizedBox(height: 10), + Text(localeMsg.objType), + const SizedBox(width: 20), SizedBox( - height: 270, child: getViewForm(localeMsg)), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - }, - label: const Text("OK"), - icon: - const Icon(Icons.thumb_up, size: 16)) - ], - ) + height: 35, + width: 147, + child: DropdownButtonFormField( + isExpanded: true, + borderRadius: BorderRadius.circular(12.0), + decoration: GetFormInputDecoration( + false, + null, + icon: Icons.bookmark, + ), + value: _objCategory, + items: getCategoryMenuItems(), + onChanged: null), + ), ], ), - ), + const SizedBox(height: 10), + SizedBox(height: 270, child: getViewForm(localeMsg)), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + }, + label: const Text("OK"), + icon: const Icon(Icons.thumb_up, size: 16)) + ], + ) + ], ), - ))), + ), + ), + )), ), ), ); @@ -172,7 +164,7 @@ class _ViewObjectPopupState extends State { color: Colors.black, child: Padding( padding: const EdgeInsets.all(8.0), - child: Text( + child: SelectableText( _loadFileResult!, style: const TextStyle(color: Colors.white), ), diff --git a/APP/pubspec.lock b/APP/pubspec.lock index 4efa820a9..40c4d5467 100644 --- a/APP/pubspec.lock +++ b/APP/pubspec.lock @@ -421,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + graphview: + dependency: "direct main" + description: + name: graphview + sha256: bdba183583b23c30c71edea09ad5f0beef612572d3e39e855467a925bd08392f + url: "https://pub.dev" + source: hosted + version: "1.2.0" html: dependency: transitive description: diff --git a/APP/pubspec.yaml b/APP/pubspec.yaml index c2c7b0d59..83d0eb685 100644 --- a/APP/pubspec.yaml +++ b/APP/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: flex_color_picker: ^3.3.1 flag: ^7.0.0 flutter_inappwebview: ^6.0.0 + graphview: ^1.2.0 dev_dependencies: mockito: ^5.4.4