From 663b24bd40ba7bb45a879fa0dcd27d30c2669ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Poderoso?= <120394830+JesusPoderoso@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:58:55 +0100 Subject: [PATCH] Domain graph view implementation (#200) * Disable dragging elements out of their bounds Signed-off-by: JesusPoderoso * Refs #19532: Implement domain graph view Signed-off-by: JesusPoderoso Refs #19308: Improve domain view Signed-off-by: JesusPoderoso Refs #19308: Adapt to new JSON and visual improvements Signed-off-by: JesusPoderoso Refs #19308: Add arrows to connections Signed-off-by: JesusPoderoso Refs #19308: Round elements Signed-off-by: JesusPoderoso Refs #19308: Fix entities height Signed-off-by: JesusPoderoso Refs #19308: Update tab name with view content Signed-off-by: JesusPoderoso Refs #19308: Fix connections Signed-off-by: JesusPoderoso Refs #19308: Add domain id selector for graph view Signed-off-by: JesusPoderoso Refs #19308: Add Refresh button and refresh feature Signed-off-by: JesusPoderoso * Refs #19532: Implement Monitor <---> Backend connection Signed-off-by: JesusPoderoso Refs #19308: Establish connection Signed-off-by: JesusPoderoso Refs #19308: Add try-catch in SyncBackendConnection call Signed-off-by: JesusPoderoso Refs #19308: Catch Exception when no graph Signed-off-by: JesusPoderoso * Refs #19532: Fix issues, visual bugs and improvements Signed-off-by: JesusPoderoso Refs #19308: Fix communication arrows visual bugs Signed-off-by: JesusPoderoso Refs #19308: Fix icons and text spacing in entity boxes Signed-off-by: JesusPoderoso Refs: Remove vulcanexus blue from color palette Signed-off-by: JesusPoderoso Refs #19308: Fix entity order generation to optimize resizing Signed-off-by: JesusPoderoso Refs #19308: Add comments to QML code Signed-off-by: JesusPoderoso Refs #19308: Improve backend calls Signed-off-by: JesusPoderoso Refs #19308: Fix overlapping visual bugs Signed-off-by: JesusPoderoso Refs #19308: Fix resizing with large alias Signed-off-by: JesusPoderoso Refs #19308: Fix single topic (and no entities) data representation Signed-off-by: JesusPoderoso Refs #19308: Fix tab issue with multiple domains Signed-off-by: JesusPoderoso Refs #19308: Display domain info when navigating through different domain views in tabs Signed-off-by: JesusPoderoso Refs #19308: Display entity and topic data when clicked in the graph Signed-off-by: JesusPoderoso Refs #19513: Improve Domain ID Dialog values Signed-off-by: JesusPoderoso Refs #19513: Add metatraffic filter in the graph view Signed-off-by: JesusPoderoso Refs #19533: Improve arrow drawing Signed-off-by: JesusPoderoso * Refs #19532: Please linters Signed-off-by: JesusPoderoso * Initial empty commit Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary include Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary eol ';' Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove 'magic numbers' Signed-off-by: JesusPoderoso Refs #19532: [ARS] Remove wheel displacement 'magic number' Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Simplify hidden scrollbar Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecesary item Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Update status comparision to uppercase Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary width conditions Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary filling rect Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Fix comment Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary recursive call Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Fix refresh domain info call Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary stack anchoring Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary 'z' ordering Signed-off-by: JesusPoderoso Refs #19532: [ARS] Remove unnecessary 'z' ordering Signed-off-by: JesusPoderoso * Refs #19532: [ARS] Remove unnecessary load model calls Signed-off-by: JesusPoderoso * Refs #19532: Reorder elements to fix tabs overlapping Signed-off-by: JesusPoderoso * Refs #19532: Fix warning displayed when showing metatraffic Signed-off-by: JesusPoderoso * Refs #19533: [ARS] Add parenthesys to conditional assignations Signed-off-by: JesusPoderoso * Refs #19533: [ARS] Add error check in missing entities Signed-off-by: JesusPoderoso * Refs #19533: [ARS] Remove unnecessary rect Signed-off-by: JesusPoderoso * Refs #19533: [ARS] Add missing space Signed-off-by: JesusPoderoso * Refs #19533: [ARS] Improve entities width management Signed-off-by: JesusPoderoso * Refs #19533: Fix connections Signed-off-by: JesusPoderoso * Refs #19743: [ARS] Improve comparasion conditions Signed-off-by: JesusPoderoso * Refs #19743: [ARS] Run resize call only once Signed-off-by: JesusPoderoso * Refs #19743: [ARS] Improve connection generation Signed-off-by: JesusPoderoso * Refs #19743: Remove unnecessary load model call Signed-off-by: JesusPoderoso * Refs #19743: Reduce amount of painting iterations Signed-off-by: JesusPoderoso * Refs #19743: Fix typo Signed-off-by: JesusPoderoso * Refs #19743: Remove unnecessary signal declaration Signed-off-by: JesusPoderoso * Refs #19743: Apply latest rev suggestions Signed-off-by: JesusPoderoso --------- Signed-off-by: JesusPoderoso --- imports/Theme/Theme.qml | 1 + include/fastdds_monitor/Controller.h | 5 + include/fastdds_monitor/Engine.h | 4 + .../backend/SyncBackendConnection.h | 4 + qml.qrc | 2 + qml/DomainGraphLayout.qml | 1461 +++++++++++++++++ qml/EntityList.qml | 3 + qml/GraphConnection.qml | 148 ++ qml/LogicalView.qml | 2 + qml/PhysicalView.qml | 3 + qml/TabLayout.qml | 288 +++- src/Controller.cpp | 7 + src/Engine.cpp | 6 + src/backend/SyncBackendConnection.cpp | 16 + 14 files changed, 1873 insertions(+), 77 deletions(-) create mode 100644 qml/DomainGraphLayout.qml create mode 100644 qml/GraphConnection.qml diff --git a/imports/Theme/Theme.qml b/imports/Theme/Theme.qml index eca68b50..3c5655c2 100644 --- a/imports/Theme/Theme.qml +++ b/imports/Theme/Theme.qml @@ -11,6 +11,7 @@ QtObject { readonly property color grey: "#808080" readonly property color lightGrey: "#d3d3d3" + readonly property color darkGrey: "#3e3e3e" readonly property color x11Grey: "#BEBEBE" readonly property color whiteSmoke: "#f5f5f5" diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index c47017a8..17f61ace 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -260,6 +260,11 @@ public slots: quint64 series_id, quint64 new_max_point); + + //! Request to backend the latest domain view JSON to build the graph + QString get_domain_view_graph ( + QString domain_id); + signals: //! Signal to show the Error Dialog diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index f194cdc5..129b6d95 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -495,6 +495,10 @@ class Engine : public QQmlApplicationEngine quint64 series_id, quint64 new_max_point); + //! Request to backend the latest domain view JSON to build the graph + backend::Graph get_domain_view_graph ( + const backend::EntityId& domain_id); + signals: /** diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index bfda3e27..ee4ab907 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -257,6 +257,10 @@ class SyncBackendConnection std::vector& source_ids, std::vector& target_ids); + //! Request to backend the latest domain view JSON to build the graph + Graph get_domain_view_graph ( + const EntityId& domain_id); + protected: void change_unit_magnitude( diff --git a/qml.qrc b/qml.qrc index 9901bc52..22715c9e 100644 --- a/qml.qrc +++ b/qml.qrc @@ -21,6 +21,7 @@ qml/CustomLegend.qml qml/CustomScrollBar.qml qml/DifferClickMouseArea.qml + qml/DomainGraphLayout.qml qml/DoubleSpinBox.qml qml/DumpFileDialog.qml qml/DynamicDataKindDialog.qml @@ -30,6 +31,7 @@ qml/EntityList.qml qml/ErrorDialog.qml qml/ExportCSVFileDialog.qml + qml/GraphConnection.qml qml/HistoricDataKindDialog.qml qml/HistoricDisplayStatisticsDialog.qml qml/HistoricStatisticsChartView.qml diff --git a/qml/DomainGraphLayout.qml b/qml/DomainGraphLayout.qml new file mode 100644 index 00000000..5aadb1fa --- /dev/null +++ b/qml/DomainGraphLayout.qml @@ -0,0 +1,1461 @@ +// Copyright 2023 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// This file is part of eProsima Fast DDS Monitor. +// +// eProsima Fast DDS Monitor is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// eProsima Fast DDS Monitor is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with eProsima Fast DDS Monitor. If not, see . + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Theme 1.0 + +Item +{ + id: domainGraphLayout + + // Public properties + property var model: {} // domain view graph JSON model + property int entity_id // entity id associated to the domain id + property int domain_id // domain id + required property string component_id // mandatory to be included when object created + + // Public signals + signal update_tab_name(string new_name) // Update tab name based on selected domain id + + // Private properties + property var topic_locations_: {} // topic information needed for connection representation + property var endpoint_topic_connections_: {} // endpoint information needed for connection representation + property var topic_painted_: [] // already painted topic connection references + property var endpoint_painted_: [] // already painted endpoint connection references + property var pending_endpoints_: [] // pending endpoints references that have not been resized yet + property var pending_connections_: [] // pending connections references that have not been generated yet + property int entity_box_width_: 0 // entities box width management + + // Private (resize) signals The signal resize_elements_ will trigger all entities resize methods in + // HOST TOPIC ─┐ the order displayed in the left figure. All entities width value are + // 1↓ ... ↓4 5↑ 6└─>CONNECTIONS based on the var entity_box_width which would be updated with the max + // ENDPOINT ────┘ width. After that, connections between endpoints and topics are generated. + signal resize_elements_() + signal topics_updated_() + signal endpoints_updated_() + signal record_connections_() + + // Read only design properties (sizes and colors) + readonly property int radius_: 10 + readonly property int connection_thickness_: 5 + readonly property int elements_spacing_: 12 + readonly property int containers_spacing_: 100 + readonly property int endpoint_height_: 40 + readonly property int first_indentation_: 5 + readonly property int icon_size_: 18 + readonly property int label_height_: 35 + readonly property int spacing_icon_label_: 8 + readonly property int scrollbar_min_size_: 8 + readonly property int scrollbar_max_size_: 12 + readonly property int topic_thickness_: 10 + readonly property int wheel_displacement_: 30 + readonly property string topic_color_: Theme.grey + readonly property string host_color_: Theme.darkGrey + readonly property string user_color_: Theme.eProsimaLightBlue + readonly property string process_color_: Theme.eProsimaDarkBlue + readonly property string participant_color_: Theme.whiteSmoke + readonly property string reader_color_: Theme.eProsimaYellow + readonly property string writer_color_: Theme.eProsimaGreen + + // Horizontal scroll view for topics section. This will contain also a Flickable that replicates entities height + // and will move accordingly to display the connections + Flickable { + id: topicView + anchors.top: parent.top; anchors.bottom: parent.bottom + anchors.left: parent.left; anchors.leftMargin: entity_box_width_ + elements_spacing_ + width: parent.width - entity_box_width_ - 2*elements_spacing_ + flickableDirection: Flickable.HorizontalFlick + boundsBehavior: Flickable.StopAtBounds + + contentWidth: topicsList.contentWidth + containers_spacing_ + contentHeight: parent.height + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOff } + ScrollBar.horizontal: ScrollBar { + id: horizontal_bar + anchors.left: parent.left; anchors.leftMargin: elements_spacing_ + anchors.bottom: parent.bottom + policy: ScrollBar.AlwaysOn + visible: topicView.contentWidth > topicView.width + hoverEnabled: true + + contentItem: Item { + implicitHeight: scrollbar_min_size_ + + Rectangle { + anchors.fill: parent + anchors.rightMargin: scrollbar_max_size_ + anchors.leftMargin: 1 + anchors.topMargin: 2 + anchors.bottomMargin: 2 + radius: height / 2 + color: horizontal_bar.pressed ? Theme.eProsimaLightBlue : Theme.lightGrey + } + } + + background: Item { + implicitHeight: scrollbar_max_size_ + + Rectangle { + anchors.fill: parent + color: horizontal_bar.pressed ? Theme.lightGrey : Theme.grey + } + } + } + + // List view of topics model + ListView + { + id: topicsList + model: domainGraphLayout.model ? domainGraphLayout.model["topics"] : undefined + anchors.left: parent.left; anchors.leftMargin: 2 * elements_spacing_ + anchors.top: parent.top; anchors.topMargin: elements_spacing_; + anchors.bottom: parent.bottom + contentWidth: contentItem.childrenRect.width + spacing: elements_spacing_ + orientation: ListView.Horizontal + interactive: false + + // Resizing management connections + Connections + { + target: domainGraphLayout + + function onEndpoints_updated_() + { + topicsList.record_connections() + } + } + + // Resize performed also when new element included in the model + onCountChanged: + { + topicsList.resize() + } + + // Calculates the list height based on the number of contained entities, and width based on their widths + function resize() + { + var listViewHeight = 0 + var listViewWidth = 0 + + // iterate over each element in the list item + for (var c = 0; c < topicsList.count; c++) + { + topicsList.currentIndex = c + if (topicsList.currentItem != null) + { + listViewHeight = topicsList.currentItem.height + listViewWidth += topicsList.currentItem.width + elements_spacing_ + } + } + topicsList.height = listViewHeight + topicsList.width = listViewWidth + } + + function record_connections() + { + var draw_width = 2*elements_spacing_ + + // load topic sizes + topicsList.resize() + + // iterate over each element in the list item + for (var c = 0; c < topicsList.count; c++) + { + topicsList.currentIndex = c + topic_locations_[topicsList.currentItem.topic_id] = { + "id": topicsList.currentItem.topic_id, + "width" : draw_width + topicsList.currentItem.width/2 + } + draw_width += topicsList.currentItem.width + elements_spacing_ + } + + // announce topics are ready + topics_updated_() + } + + // Topic delegated item box with vertical line + delegate: Rectangle + { + property string topic_id: modelData["id"] + implicitWidth: topic_tag.implicitWidth + height: topicsList.height + color: "transparent" + + // Topic name and icon + Rectangle + { + id: topic_tag + implicitWidth: topicRowLayout.implicitWidth + height: label_height_ + color: topic_color_ + radius: radius_ + + RowLayout { + id: topicRowLayout + spacing: spacing_icon_label_ + anchors.centerIn: parent + + IconSVG { + name: "topic" + color: "white" + size: icon_size_ + Layout.leftMargin: first_indentation_ + } + Label { + text: modelData["alias"] + Layout.rightMargin: 2* first_indentation_ + color: "white" + } + } + MouseArea + { + anchors.fill: parent + onClicked: + { + controller.topic_click(modelData["id"]) + } + } + } + + // Topic vertical line + Rectangle + { + id: topic_down_bar + anchors.top: topic_tag.bottom + anchors.bottom: parent.bottom + anchors.horizontalCenter: topic_tag.horizontalCenter + width: topic_thickness_ + color: topic_color_ + } + } + } + + // Section where connections are represented + Flickable + { + id: topicSpace + anchors.top: parent.top; anchors.topMargin: label_height_ + 2* elements_spacing_ + anchors.left: parent.left + width: parent.width + height: parent.height - (label_height_ + 2* elements_spacing_) + interactive: false + clip: true + + contentWidth: topicsList.contentWidth + containers_spacing_ + contentHeight: mainView.contentHeight + Rectangle {id: topic_connections; anchors.fill:parent; color: "transparent" } + + // Not visible scroll bar + ScrollBar.vertical: ScrollBar{ + id: custom_bar + visible: false + + // connection to move vertically the view when entities view moves vertically + Connections { + target: vertical_bar + + function onPositionChanged(){ + custom_bar.position = vertical_bar.position + } + } + } + + // Overriding mouse area to scroll horizontally on wheel event + MouseArea { + anchors.fill: parent + preventStealing: true + + onWheel: { + if (topicView.contentWidth > topicView.width) + { + if (wheel.angleDelta.y > 0) { + topicView.contentX -= wheel_displacement_ + if (topicView.contentX < 0) { + topicView.contentX = 0; + } + } else { + topicView.contentX += wheel_displacement_ + if ((topicView.contentX + topicView.width) > topicView.contentWidth) { + topicView.contentX = topicView.contentWidth - topicView.width; + } + } + } + } + onClicked: mouse.accepted = false; + onPressed: mouse.accepted = false; + onReleased: mouse.accepted = false; + onDoubleClicked: mouse.accepted = false; + onPositionChanged: mouse.accepted = false; + onPressAndHold: mouse.accepted = false; + } + } + + // Resizing management connections + Connections + { + target: domainGraphLayout + + function onTopics_updated_() + { + topicView.generate_connections() + } + } + + // Generate connections in topic side + function generate_connections() + { + for (var key in endpoint_topic_connections_) + { + var topic_id = endpoint_topic_connections_[key]["destination_id"] + if (topic_locations_[topic_id] != undefined) + { + if (!topic_painted_.includes(key)) + { + var input = {"x": 0 + ,"right_direction": endpoint_topic_connections_[key]["right_direction"] + ,"y": endpoint_topic_connections_[key]["y"] - (connection_thickness_ / 2) + ,"width": topic_locations_[topic_id]["width"] + ,"height":connection_thickness_, "z":200, "left_margin": 2*elements_spacing_ + ,"arrow_color": topic_color_, "background_color": background_color.color } + var connection_bar = arrow_component.createObject(topic_connections, input) + topic_painted_[topic_painted_.length] = key; + } + } + } + } + } + + // Entities vertical flickable (left section) + Flickable { + id: mainView + anchors.left: parent.left ; anchors.top: parent.top; anchors.bottom: parent.bottom + width: entity_box_width_ + elements_spacing_ + anchors.topMargin: 2* elements_spacing_ + label_height_ + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + + contentWidth: mainSpace.width + contentHeight: mainSpace.height + + ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOff } + ScrollBar.vertical: ScrollBar { + id: vertical_bar + policy: ScrollBar.AlwaysOn + visible: mainView.contentHeight > mainView.height + anchors.top: parent.top; anchors.topMargin: -elements_spacing_ + anchors.right: parent.right; anchors.rightMargin: parent.width - domainGraphLayout.width + hoverEnabled: true + + contentItem: Item { + implicitWidth: scrollbar_min_size_ + + Rectangle { + anchors.fill: parent + anchors.topMargin: 1 + anchors.rightMargin: 2 + anchors.leftMargin: 2 + radius: width / 2 + color: vertical_bar.pressed ? Theme.eProsimaLightBlue : Theme.lightGrey + } + } + + background: Item { + implicitWidth: scrollbar_max_size_ + + Rectangle { + anchors.fill: parent + color: vertical_bar.pressed ? Theme.lightGrey : Theme.grey + } + } + + Rectangle { + anchors.top: parent.top + height: 1 + width: parent.width + color: vertical_bar.pressed ? Theme.lightGrey : Theme.grey + } + } + + // Scpace where entities will be represented + Rectangle + { + id: mainSpace + anchors.top: parent.top + + width: hostsList.width + 2*elements_spacing_ + height: hostsList.height < (domainGraphLayout.height - (label_height_ + 2*elements_spacing_)) + ? domainGraphLayout.height - (label_height_ + 2*elements_spacing_) : hostsList.height + + // Entities background + Rectangle { + id: background_color + anchors.fill: parent + color: "white" + } + + // Graph connection component definition for object creation purposes + Component { + id: arrow_component + GraphConnection{ + + } + } + + // List view of hosts model (which would contain remain entities nested) + ListView + { + id: hostsList + model: domainGraphLayout.model ? domainGraphLayout.model["hosts"] : undefined + anchors.top: parent.top + anchors.left: parent.left; anchors.leftMargin: elements_spacing_ + interactive: false + spacing: elements_spacing_ + + // Resizing management connections + Connections + { + target: domainGraphLayout + + function onResize_elements_() + { + hostsList.resize() + hostsList.resize_elements() + } + } + + // Resize performed also when new element included in the model + onCountChanged: + { + hostsList.resize() + } + + // Calculates the list height based on the number of contained entities, and width based on their widths + function resize() + { + var listViewHeight = 0 + var aux_width = entity_box_width_ + + // iterate over each element in the list item + for (var c = 0; c < hostsList.count; c++) + { + hostsList.currentIndex = c + if (hostsList.currentItem != null) + { + listViewHeight += hostsList.currentItem.height + elements_spacing_ + aux_width = Math.max(aux_width, hostsList.currentItem.width) + } + } + + hostsList.height = listViewHeight + + // update if necessary + if (aux_width > entity_box_width_) + { + entity_box_width_ = aux_width + } + } + + // Makes each list element to be resized + function resize_elements() + { + // iterate over each element in the list item + for (var c = 0; c < hostsList.count; c++) + { + hostsList.currentIndex = c + if (hostsList.currentItem != null) + { + hostsList.currentItem.resize() + } + } + } + + // Host delegated item box + delegate: Item + { + height: host_tag.height + usersList.height + width: hostRowLayout.implicitWidth > entity_box_width_ + ? hostRowLayout.implicitWidth + : entity_box_width_ + + function resize() + { + usersList.resize() + usersList.resize_elements() + } + + // background + Rectangle + { + id: host_background + height: parent.height + width: parent.width + color: host_color_ + radius: radius_ + } + + // host name and icons + Rectangle + { + id: host_tag + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: hostRowLayout.implicitWidth > entity_box_width_ + ? hostRowLayout.implicitWidth + : entity_box_width_ + height: label_height_ + color: host_color_ + radius: radius_ + + RowLayout { + id: hostRowLayout + spacing: spacing_icon_label_ + anchors.centerIn: parent + + Rectangle { + color: "transparent" + width: modelData["status"] != "OK" + ? first_indentation_ : 0 + } + IconSVG { + visible: modelData["status"] != "OK" + name: "issues" + color: "white" + size: modelData["status"] != "OK"? icon_size_ : 0 + } + Rectangle { + color: "transparent" + width: first_indentation_ /2 + } + IconSVG { + name: "host" + color: "white" + size: icon_size_ + } + Label { + text: modelData["alias"] + Layout.rightMargin: first_indentation_ + color: "white" + } + } + MouseArea + { + anchors.fill: parent + onClicked: + { + controller.host_click(modelData["id"]) + } + } + } + + // List view of users model (which would contain remain entities nested) + ListView + { + id: usersList + model: modelData["users"] + anchors.top: host_tag.bottom; anchors.topMargin: elements_spacing_ + anchors.left: parent.left; anchors.leftMargin: elements_spacing_ + anchors.right: parent.right; anchors.rightMargin: elements_spacing_ + interactive: false + spacing: elements_spacing_ + + // Resize performed also when new element included in the model + onCountChanged: + { + usersList.resize() + } + + // Calculates the list height based on the number of contained entities, and width based on + // their widths + function resize() + { + var listViewHeight = 0 + var aux_width = entity_box_width_ + + // iterate over each element in the list item + for (var c = 0; c < usersList.count; c++) + { + usersList.currentIndex = c + if (usersList.currentItem != null) + { + listViewHeight += usersList.currentItem.height + elements_spacing_ + aux_width = Math.max(aux_width, usersList.currentItem.width+(2*elements_spacing_)) + } + } + + usersList.height = listViewHeight + elements_spacing_ + + // update if necessary + if (aux_width > entity_box_width_) + { + entity_box_width_ = aux_width + } + } + + // Makes each list element to be resized + function resize_elements() + { + // iterate over each element in the list item + for (var c = 0; c < usersList.count; c++) + { + usersList.currentIndex = c + if (usersList.currentItem != null) + { + usersList.currentItem.resize() + } + } + } + + + // User delegated item box + delegate: Item + { + height: user_tag.height + processesList.height + width: userRowLayout.implicitWidth > (entity_box_width_-(2*elements_spacing_)) + ? userRowLayout.implicitWidth + : entity_box_width_-(2*elements_spacing_) + + function resize() + { + processesList.resize() + processesList.resize_elements() + } + + // background + Rectangle + { + id: user_background + height: parent.height + width: parent.width + color: user_color_ + radius: radius_ + } + + // user name and icons + Rectangle + { + id: user_tag + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: userRowLayout.implicitWidth > (entity_box_width_-(2*elements_spacing_)) + ? userRowLayout.implicitWidth + : entity_box_width_-(2*elements_spacing_) + height: label_height_ + color: user_color_ + radius: radius_ + + RowLayout { + id: userRowLayout + spacing: spacing_icon_label_ + anchors.centerIn: parent + + Rectangle { + color: "transparent" + width: modelData["status"] != "OK" + ? first_indentation_ : 0 + } + IconSVG { + visible: modelData["status"] != "OK" + name: "issues" + color: "white" + size: modelData["status"] != "OK"? icon_size_ : 0 + } + Rectangle { + color: "transparent" + width: first_indentation_ /2 + } + IconSVG { + name: "user" + color: "white" + size: icon_size_ + } + Label { + text: modelData["alias"] + Layout.rightMargin: first_indentation_ + color: "white" + } + } + MouseArea + { + anchors.fill: parent + onClicked: + { + controller.user_click(modelData["id"]) + } + } + } + + // List view of processes model (which would contain remain entities nested) + ListView + { + id: processesList + model: modelData["processes"] + anchors.top: user_tag.bottom; anchors.topMargin: elements_spacing_ + anchors.left: parent.left; anchors.leftMargin: elements_spacing_ + anchors.right: parent.right; anchors.rightMargin: elements_spacing_ + interactive: false + spacing: elements_spacing_ + + // Resize performed also when new element included in the model + onCountChanged: + { + processesList.resize() + } + + // Calculates the list height based on the number of contained entities, + // and width based on their widths + function resize() + { + var listViewHeight = 0 + var aux_width = entity_box_width_ + + // iterate over each element in the list item + for (var c = 0; c < processesList.count; c++) + { + processesList.currentIndex = c + if (processesList.currentItem != null) + { + listViewHeight += processesList.currentItem.height + elements_spacing_ + aux_width = Math.max(aux_width, processesList.currentItem.width+(4*elements_spacing_)) + } + } + + processesList.height = listViewHeight + elements_spacing_ + + // update if necessary + if (aux_width > entity_box_width_) + { + entity_box_width_ = aux_width + } + } + + // Makes each list element to be resized + function resize_elements() + { + // iterate over each element in the list item + for (var c = 0; c < processesList.count; c++) + { + processesList.currentIndex = c + if (processesList.currentItem != null) + { + processesList.currentItem.resize() + } + } + } + + // Process delegated item box + delegate: Item + { + height: process_tag.height + participantsList.height + width: processRowLayout.implicitWidth > (entity_box_width_-(4*elements_spacing_)) + ? processRowLayout.implicitWidth + : entity_box_width_-(4*elements_spacing_) + + function resize() + { + participantsList.resize() + participantsList.resize_elements() + } + + // background + Rectangle + { + id: process_background + height: parent.height + width: parent.width + color: process_color_ + radius: radius_ + } + + // process name and icons + Rectangle + { + id: process_tag + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: processRowLayout.implicitWidth > (entity_box_width_-(4*elements_spacing_)) + ? processRowLayout.implicitWidth + : entity_box_width_-(4*elements_spacing_) + height: label_height_ + color: process_color_ + radius: radius_ + + RowLayout { + id: processRowLayout + spacing: spacing_icon_label_ + anchors.centerIn: parent + + Rectangle { + color: "transparent" + width: modelData["status"] != "OK" + ? first_indentation_ : 0 + } + IconSVG { + visible: modelData["status"] != "OK" + name: "issues" + color: "white" + size: modelData["status"] != "OK"? icon_size_ : 0 + } + Rectangle { + color: "transparent" + width: first_indentation_ /2 + } + IconSVG { + name: "process" + color: "white" + size: icon_size_ + } + Label { + text: modelData["alias"] + Layout.rightMargin: first_indentation_ + color: "white" + } + } + MouseArea + { + anchors.fill: parent + onClicked: + { + controller.process_click(modelData["id"]) + } + } + } + + // List view of participants model (which would contain remain endpoints nested) + ListView + { + id: participantsList + model: modelData["participants"] + anchors.top: process_tag.bottom; anchors.topMargin: elements_spacing_ + anchors.left: parent.left; anchors.leftMargin: elements_spacing_ + anchors.right: parent.right; anchors.rightMargin: elements_spacing_ + interactive: false + spacing: elements_spacing_ + + // Resize performed also when new element included in the model + onCountChanged: + { + participantsList.resize() + } + + // Calculates the list height based on the number of contained entities, + // and width based on their widths + function resize() + { + var listViewHeight = 0 + var aux_width = entity_box_width_ + + // iterate over each element in the list item + for (var c = 0; c < participantsList.count; c++) + { + participantsList.currentIndex = c + if (participantsList.currentItem != null) + { + listViewHeight += participantsList.currentItem.height + elements_spacing_ + aux_width = Math.max(aux_width, participantsList.currentItem.width+(6*elements_spacing_)) + } + } + + participantsList.height = listViewHeight + elements_spacing_ + + // update if necessary + if (aux_width > entity_box_width_) + { + entity_box_width_ = aux_width + } + } + + // Makes each list element to be resized + function resize_elements() + { + // iterate over each element in the list item + for (var c = 0; c < participantsList.count; c++) + { + participantsList.currentIndex = c + if (participantsList.currentItem != null) + { + participantsList.currentItem.resize() + } + } + } + + // Participant delegated item box + delegate: Item + { + height: participant_tag.height + endpointsList.height + width: participantRowLayout.implicitWidth > (entity_box_width_-(6*elements_spacing_)) + ? participantRowLayout.implicitWidth + : entity_box_width_-(6*elements_spacing_) + + function resize() + { + endpointsList.resize() + endpointsList.resize_elements() + } + + // background + Rectangle + { + id: participant_background + height: parent.height + width: parent.width + color: participant_color_ + radius: radius_ + } + + // participant name and icons + Rectangle + { + id: participant_tag + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: participantRowLayout.implicitWidth > (entity_box_width_-(6*elements_spacing_)) + ? participantRowLayout.implicitWidth + : entity_box_width_-(6*elements_spacing_) + height: label_height_ + color: participant_color_ + radius: radius_ + + RowLayout { + id: participantRowLayout + spacing: spacing_icon_label_ + anchors.centerIn: parent + + Rectangle { + color: "transparent" + width: modelData["status"] != "OK" + ? first_indentation_ : 0 + } + IconSVG { + visible: modelData["status"] != "OK" + name: "issues" + size: modelData["status"] != "OK"? icon_size_ : 0 + } + Rectangle { + color: "transparent" + width: first_indentation_ /2 + } + IconSVG { + name: modelData["kind"] + size: icon_size_ + } + Label { + text: modelData["alias"] + Layout.rightMargin: spacing_icon_label_ + first_indentation_ + } + } + MouseArea + { + anchors.fill: parent + onClicked: + { + controller.participant_click(modelData["id"]) + } + } + } + + // List view of endpoint model + ListView + { + id: endpointsList + model: modelData["endpoints"] + anchors.top: participant_tag.bottom; anchors.topMargin: elements_spacing_ + anchors.left: parent.left; anchors.leftMargin: elements_spacing_ + anchors.right: parent.right; anchors.rightMargin: elements_spacing_ + interactive: false + spacing: elements_spacing_ + + // Connection management + Connections + { + target: domainGraphLayout + + function onRecord_connections_() + { + endpointsList.record_connections() + } + } + + // Resize performed also when new element included in the model + onCountChanged: + { + endpointsList.resize() + } + + // Calculates the list height based on the number of contained entities, + // and width based on their widths + function resize() + { + var listViewHeight = 0 + var aux_width = entity_box_width_ + + // iterate over each element in the list item + for (var c = 0; c < endpointsList.count; c++) + { + endpointsList.currentIndex = c + if (endpointsList.currentItem != null) + { + listViewHeight += endpointsList.currentItem.height + elements_spacing_ + aux_width = Math.max(aux_width, endpointsList.currentItem.width+(8*elements_spacing_)) + } + } + + endpointsList.height = listViewHeight + elements_spacing_ + + // update if necessary + if (aux_width > entity_box_width_) + { + entity_box_width_ = aux_width + } + } + + // Makes each list element to be resized + function resize_elements() + { + // remove current endpoints from pending queue + for (var c = 0; c < endpointsList.count; c++) + { + endpointsList.currentIndex = c + if (endpointsList.currentItem != null) + { + if (pending_endpoints_.includes(endpointsList.currentItem.get_endpoint_id())) + { + pending_connections_[pending_connections_.length] = endpointsList.currentItem.get_endpoint_id() + pending_endpoints_.splice(pending_endpoints_.indexOf(endpointsList.currentItem.get_endpoint_id()), 1) + } + } + } + + if (pending_endpoints_.length == 0) + { + domainGraphLayout.record_connections_() + } + } + + function record_connections() + { + for (var c = 0; c < endpointsList.count; c++) + { + endpointsList.currentIndex = c + if (pending_connections_.includes(endpointsList.currentItem.get_endpoint_id())) + { + pending_connections_.splice(pending_connections_.indexOf(endpointsList.currentItem.get_endpoint_id()), 1) + endpointsList.currentItem.record_connection() + } + } + + if (pending_connections_.length == 0) + { + stop_timer() + endpoints_updated_() + } + } + + // Endpoint delegated item box + delegate: Item + { + id: endpointComponent + width: endpointRowLayout.implicitWidth > (entity_box_width_-(8*elements_spacing_)) + ? endpointRowLayout.implicitWidth + : entity_box_width_-(8*elements_spacing_) + height: endpoint_height_ + + // Saves the endpoint needed info for connection representation + function record_connection() + { + var globalCoordinates = endpointComponent.mapToItem(mainSpace, 0, 0) + var src_x = globalCoordinates.x + entity_box_width_-(8*elements_spacing_) + var src_y = modelData["accum_y"] + (endpointComponent.height / 2) + var left_direction = modelData["kind"] == "datareader" + var right_direction = modelData["kind"] == "datawriter" + + endpoint_topic_connections_[modelData["id"]] = { + "id": modelData["id"], "left_direction": left_direction, + "right_direction": right_direction, "x": src_x, "y": src_y, + "destination_id": modelData["topic"] + } + } + + function get_endpoint_id() + { + return modelData["id"] + } + + // background + Rectangle + { + id: endpoint_background + width: parent.width + height: endpoint_height_ + color: modelData["kind"] == "datareader" ? reader_color_ : writer_color_ + radius: radius_ + } + + // endpoint name and icons + Rectangle + { + id: endpoint_tag + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: endpointRowLayout.implicitWidth > (entity_box_width_-(8*elements_spacing_)) + ? endpointRowLayout.implicitWidth + : entity_box_width_-(8*elements_spacing_) + height: endpoint_height_ + color: endpoint_background.color + radius: radius_ + + RowLayout { + id: endpointRowLayout + spacing: spacing_icon_label_ + anchors.centerIn: parent + + Rectangle { + color: "transparent" + width: modelData["status"] != "OK" + ? first_indentation_ : 0 + } + IconSVG { + visible: modelData["status"] != "OK" + name: "issues" + size: modelData["status"] != "OK"? icon_size_ : 0 + } + Rectangle { + color: "transparent" + width: first_indentation_ /2 + } + IconSVG { + name: modelData["kind"] + size: icon_size_ + } + Label { + text: modelData["alias"] + Layout.rightMargin: spacing_icon_label_ + first_indentation_ + } + } + MouseArea + { + anchors.fill: parent + onClicked: + { + controller.endpoint_click(modelData["id"]) + } + } + } + } + } + } + } + } + } + } + } + } + } + + // Resizing management connections + Connections + { + target: domainGraphLayout + + function onTopics_updated_() + { + mainSpace.generate_connections() + } + } + + // Saves the topic needed info for connection representation + function generate_connections() + { + for (var key in endpoint_topic_connections_) + { + var topic_id = endpoint_topic_connections_[key]["destination_id"] + if (topic_locations_[topic_id] != undefined) + { + if (!endpoint_painted_.includes(key)) + { + var input = {"x": endpoint_topic_connections_[key]["x"] + ,"y": endpoint_topic_connections_[key]["y"] - (connection_thickness_ / 2) + ,"left_direction": endpoint_topic_connections_[key]["left_direction"] + ,"width": 5*elements_spacing_ + ,"height":connection_thickness_, "z":200 + ,"arrow_color": topic_color_, "background_color": background_color.color } + var connection_bar = arrow_component.createObject(mainSpace, input) + endpoint_painted_[endpoint_painted_.length] = key + } + } + } + } + } + } + + // top section to cut entities layout and display the REFRESH butotn + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + height: 2* elements_spacing_ + label_height_ + width: entity_box_width_ + 2*elements_spacing_ + color: "white" + + // Refresh button + Button{ + id: refresh_button + width: (parent.width /2) < 150 ? 150 : parent.width /2 + height: label_height_ + anchors.top: parent.top; anchors.topMargin: elements_spacing_ + anchors.left: parent.left + anchors.leftMargin: ((entity_box_width_/2) + elements_spacing_ - (refresh_button.width /2)) < 40 + ? 5* elements_spacing_ : (entity_box_width_/2) + elements_spacing_ - (refresh_button.width /2) + text: "Refresh" + + onClicked:{ + load_model() + } + } + } + + // footer section to cut entities layout + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + height: elements_spacing_ + width: entity_box_width_ + 2*elements_spacing_ + color: "white" + z: 14 + } + + // Empty screen message + Rectangle { + anchors.fill: parent + color: "transparent" + + Text { + id: emptyScreenLabel + visible: true + width: parent.width + height: parent.height + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + fontSizeMode: Text.Fit + minimumPixelSize: 10 + font.pointSize: 20 + font.bold: true + color: Theme.x11Grey + text: "Oops... no data to display" + } + } + + Timer { + id: safety_timer + interval: 200; running: false + onTriggered: { interval += interval; load_model() } + } function stop_timer() { safety_timer.stop() } + + // Obtain given domain id graph JSON model + function load_model() + { + // clear internal models + clear_graph() + + // Obtain model from backend + var model_string = controller.get_domain_view_graph(entity_id) + + // declare obtained hosts and topics variables + var new_topics = [] + var new_hosts = [] + + // Check if obtained graph is not empty + if (model_string.length !== 0 && model_string !== "null") + { + // Parse model from string to JSON + var new_model = JSON.parse(model_string) + + // Ensure expected graph was received + if (new_model["domain"] == domain_id) + { + var is_metatraffic_visible_ = controller.metatraffic_visible(); + + // transform indexed model to array model (arrays required for the listviews) + for (var topic in new_model["topics"]) + { + var metatraffic_ = new_model["topics"][topic]["metatraffic"] + if (metatraffic_ != true || is_metatraffic_visible_) + { + new_topics[new_topics.length] = { + "id":topic, + "kind":new_model["topics"][topic]["kind"], + "alias":new_model["topics"][topic]["alias"] + } + } + } + var accum_y = 0 + for (var host in new_model["hosts"]) + { + var metatraffic_ = new_model["hosts"][host]["metatraffic"] + if (metatraffic_ != true || is_metatraffic_visible_) + { + accum_y += label_height_ + elements_spacing_ + var new_users = [] + for (var user in new_model["hosts"][host]["users"]) + { + var metatraffic_ = new_model["hosts"][host]["users"][user]["metatraffic"] + if (metatraffic_ != true || is_metatraffic_visible_) + { + accum_y += label_height_ + elements_spacing_ + var new_processes = [] + for (var process in new_model["hosts"][host]["users"][user]["processes"]) + { + var metatraffic_ = new_model["hosts"][host]["users"][user]["processes"][process]["metatraffic"] + if (metatraffic_ != true || is_metatraffic_visible_) + { + accum_y += label_height_ + elements_spacing_ + var new_participants = [] + for (var participant in new_model["hosts"][host]["users"][user]["processes"][process]["participants"]) + { + var metatraffic_ = new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["metatraffic"] + if (metatraffic_ != true || is_metatraffic_visible_) + { + accum_y += label_height_ + elements_spacing_ + var new_endpoints = [] + for (var endpoint in new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"]) + { + var metatraffic_ = new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["metatraffic"] + if (metatraffic_ != true || is_metatraffic_visible_) + { + new_endpoints[new_endpoints.length] = { + "id":endpoint, + "kind":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["kind"], + "alias":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["alias"], + "status":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["status"], + "topic":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["topic"], + "accum_y":accum_y + } + accum_y += endpoint_height_ + elements_spacing_ + pending_endpoints_[pending_endpoints_.length] = endpoint + } + } + new_participants[new_participants.length] = { + "id":participant, + "kind":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["kind"], + "alias":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["alias"], + "status":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["status"], + "app_id":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["app_id"], + "app_metadata":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["app_metadata"], + "endpoints":new_endpoints + } + accum_y += elements_spacing_ + } + } + new_processes[new_processes.length] = { + "id":process, + "kind":new_model["hosts"][host]["users"][user]["processes"][process]["kind"], + "alias":new_model["hosts"][host]["users"][user]["processes"][process]["alias"], + "pid": new_model["hosts"][host]["users"][user]["processes"][process]["pid"], + "status":new_model["hosts"][host]["users"][user]["processes"][process]["status"], + "participants":new_participants + } + accum_y += elements_spacing_ + } + } + new_users[new_users.length] = { + "id":user, + "kind":new_model["hosts"][host]["users"][user]["kind"], + "alias":new_model["hosts"][host]["users"][user]["alias"], + "status":new_model["hosts"][host]["users"][user]["status"], + "processes":new_processes + } + accum_y += elements_spacing_ + } + } + new_hosts[new_hosts.length] = { + "id":host, + "kind":new_model["hosts"][host]["kind"], + "alias":new_model["hosts"][host]["alias"], + "status":new_model["hosts"][host]["status"], + "users":new_users + } + accum_y += elements_spacing_ + } + } + model = { + "kind": new_model["kind"], + "domain": new_model["domain"], + "topics": new_topics, + "hosts": new_hosts, + } + + // recovery timer starts + safety_timer.start() + + // Update visual elements by re-calculating their sizes + resize_elements_() + + // hide empty screen label + emptyScreenLabel.visible = false + } + } + // print error message + if (new_topics.length === 0 || new_hosts.length === 0) + { + // Discard any possible data received + model = { + "kind": "domain_view", + "domain": domain_id, + "topics": [], + "hosts": [], + } + + // display empty screen label + emptyScreenLabel.visible = true + } + + // Update tab name with selected domain id + domainGraphLayout.update_tab_name("Domain " + domain_id + " View") + } + + // remove drawn connections + function clear_graph() + { + topic_locations_ = {} + endpoint_topic_connections_ = {} + endpoint_painted_ = [] + topic_painted_ = [] + pending_endpoints_ = [] + pending_connections_ = [] + vertical_bar.position = 0 + horizontal_bar.position = 0 + entity_box_width_ = 0; + for (var i = 0; i < mainSpace.children.length; i++) + { + if (mainSpace.children[i].left_margin != undefined) + { + mainSpace.children[i].destroy() + } + } + for (var i = 0; i < topic_connections.children.length; i++) + { + if (topic_connections.children[i].left_margin != undefined) + { + topic_connections.children[i].destroy() + } + } + } +} diff --git a/qml/EntityList.qml b/qml/EntityList.qml index 3464ac98..0746d97b 100644 --- a/qml/EntityList.qml +++ b/qml/EntityList.qml @@ -47,6 +47,7 @@ Rectangle { width: parent.width height: parent.height spacing: verticalSpacing + boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: CustomScrollBar { id: scrollBar @@ -123,6 +124,7 @@ Rectangle { spacing: verticalSpacing topMargin: verticalSpacing delegate: endpointListDelegate + boundsBehavior: Flickable.StopAtBounds property int collapseHeightFlag: childrenRect.height + endpointList.topMargin } @@ -205,6 +207,7 @@ Rectangle { delegate: locatorListDelegate spacing: verticalSpacing topMargin: verticalSpacing + boundsBehavior: Flickable.StopAtBounds property int collapseHeightFlag: childrenRect.height + locatorList.topMargin } diff --git a/qml/GraphConnection.qml b/qml/GraphConnection.qml new file mode 100644 index 00000000..5464d3fc --- /dev/null +++ b/qml/GraphConnection.qml @@ -0,0 +1,148 @@ +import QtQuick 2.0 + +Item { + // public property + property bool left_direction: false // defines if the represented connection must draw a left arrow + property bool right_direction: false // defines if the represented connection must draw a right arrow + property int left_margin: 0 // left margin to be applied + property string arrow_color: Theme.grey // connection color + property string background_color: "white" // background color + + // readonly private design properties + readonly property int arrow_margin_: -4 // margins for background + readonly property int arrow_size_: 30 // arrow size + + // background to make connection overlap nicely with previous topics (looks like connection goes OVER the topic) + Rectangle { + id: background_arrow + visible: left_margin != 0 + anchors.top: parent.top; anchors.bottom: parent.bottom + anchors.topMargin: arrow_margin_; anchors.bottomMargin: arrow_margin_ + anchors.left: parent.left; anchors.right: parent.right + anchors.leftMargin: left_margin; anchors.rightMargin: left_margin; + color: background_color + } + + Rectangle { + id: left_background + opacity: 0.70 + anchors.top: parent.top; anchors.bottom: parent.bottom + anchors.topMargin: -2; anchors.bottomMargin: -2 + anchors.left: parent.left; anchors.right: parent.right + anchors.leftMargin: parent.height /2; anchors.rightMargin: 5; + color: background_color + } + + + + // left arrow if visible + Item { + id: left_arrow_background + visible: left_direction + height: arrow_size_ + 8 + width: arrow_size_ + 2 + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + + Canvas { + id: left_canvas_background + + anchors.centerIn: parent + + height: parent.height / 2 + width: parent.width + + antialiasing:true; smooth:true + + onPaint: { + var ctx = left_canvas_background.getContext('2d') + + ctx.strokeStyle = "white" + ctx.lineWidth = left_canvas_background.width * 0.1 + ctx.beginPath() + ctx.moveTo(left_canvas_background.width, left_canvas_background.height * 0.001) + ctx.lineTo(12, left_canvas_background.height / 2 - 6) + ctx.lineTo(12, left_canvas_background.height / 2 + 6) + ctx.lineTo(left_canvas_background.width, left_canvas_background.height * 0.999) + ctx.stroke() + } + } + } + + + Item { + id: left_arrow + visible: left_direction + height: arrow_size_ + width: arrow_size_ + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + + Canvas { + id: left_canvas + + anchors.centerIn: parent + + height: parent.height / 2 + width: parent.width + + antialiasing:true; smooth:true + + onPaint: { + var ctx = left_canvas.getContext('2d') + + ctx.strokeStyle = arrow_color + ctx.lineWidth = left_canvas.width * 0.1 + ctx.beginPath() + ctx.moveTo(left_canvas.width, left_canvas.height * 0.05) + ctx.lineTo(0, left_canvas.height / 2) + ctx.lineTo(left_canvas.width, left_canvas.height * 0.95) + ctx.stroke() + } + } + } + + // main connection + Rectangle { + id: base_arrow + anchors.top: parent.top; anchors.bottom: parent.bottom + anchors.left: parent.left; anchors.right: parent.right + anchors.leftMargin: left_direction ? 8 : 0 + anchors.rightMargin: right_direction ? 8 : 0 + color: arrow_color + } + + // right arrow if visible + Item { + id: right_arrow + visible: right_direction + height: arrow_size_ + width: arrow_size_ + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right; anchors.rightMargin: parent.height /2 + 2 + + Canvas { + id: right_canvas + + anchors.centerIn: parent + + height: parent.height / 2 + width: parent.width + + antialiasing:true; smooth:true + + onPaint: { + var ctx = right_canvas.getContext('2d') + + ctx.strokeStyle = arrow_color + ctx.lineWidth = right_canvas.width * 0.1 + ctx.beginPath() + ctx.moveTo(0, right_canvas.height * 0.05) + ctx.lineTo(right_canvas.width, right_canvas.height / 2 -1) + ctx.lineTo(0, right_canvas.height * 0.95) + ctx.stroke() + } + } + } +} diff --git a/qml/LogicalView.qml b/qml/LogicalView.qml index 39350711..cbbfa464 100644 --- a/qml/LogicalView.qml +++ b/qml/LogicalView.qml @@ -47,6 +47,7 @@ Rectangle { width: parent.width height: parent.height spacing: verticalSpacing + boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: CustomScrollBar { id: scrollBar @@ -129,6 +130,7 @@ Rectangle { spacing: verticalSpacing topMargin: verticalSpacing delegate: topicListDelegate + boundsBehavior: Flickable.StopAtBounds property int collapseHeightFlag: childrenRect.height + topicList.topMargin } diff --git a/qml/PhysicalView.qml b/qml/PhysicalView.qml index 35e6065d..0fa8cb55 100644 --- a/qml/PhysicalView.qml +++ b/qml/PhysicalView.qml @@ -47,6 +47,7 @@ Rectangle { width: parent.width height: parent.height spacing: verticalSpacing + boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: CustomScrollBar { id: scrollBar @@ -123,6 +124,7 @@ Rectangle { spacing: verticalSpacing topMargin: verticalSpacing delegate: userListDelegate + boundsBehavior: Flickable.StopAtBounds property int collapseHeightFlag: childrenRect.height + userList.topMargin } @@ -206,6 +208,7 @@ Rectangle { delegate: processListDelegate spacing: verticalSpacing topMargin: verticalSpacing + boundsBehavior: Flickable.StopAtBounds property int collapseHeightFlag: childrenRect.height + processList.topMargin } diff --git a/qml/TabLayout.qml b/qml/TabLayout.qml index aa1aaf52..8de5cfaf 100644 --- a/qml/TabLayout.qml +++ b/qml/TabLayout.qml @@ -29,9 +29,13 @@ Item { property int current_: 0 // current tab displayed property int last_index_: 1 // force unique idx on QML components property var tab_model_: [{"idx":0, "title":"New Tab", "stack_id": 0}] // tab model for tab bad and tab management - property bool disable_chart_selection: false // flag to disable multiple chart view tabs + property bool disable_chart_selection_: false // flag to disable multiple chart view tabs - // Read only design properties + // private signals + signal open_domain_view_(int stack_id, int entity_id, int domain_id) + signal initialize_domain_view_(int stack_id, int entity_id, int domain_id) + + // Read only design properties readonly property int max_tabs_: 15 readonly property int max_tab_size_: 180 readonly property int min_tab_size_: 120 @@ -46,12 +50,13 @@ Item { // initialize first element in the tab Component.onCompleted:{ - var new_stack = stack_component.createObject(null, {"id": 0, "anchors.fill": "parent"}) + var new_stack = stack_component.createObject(null, {"stack_id": 0}) stack_layout.children.push(new_stack) refresh_layout(current_) } ChartsLayout { + visible: disable_chart_selection_ id: chartsLayout anchors.fill: stack_layout onFullScreenChanged: { @@ -59,13 +64,151 @@ Item { } } + // stack layout (where idx referred to the tab, which would contain different views) + StackLayout { + id: stack_layout + width: tabLayout.width + anchors.top: tab_list.bottom; anchors.bottom: tabLayout.bottom + + Component { + id: stack_component + + // view with the different views available in a tab + StackView { + id: stack + property int stack_id: 0 + initialItem: view_selector + + // override push transition to none + pushEnter: Transition {} + + // menu that allows the selection of the view, and changes the stack if necessary + Component { + id: view_selector + Rectangle { + Row { + anchors{ + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + height: parent.height + width: childrenRect.width + spacing: 60 + Button { + width: 400; height: 400 + anchors.verticalCenter: parent.verticalCenter + enabled: !disable_chart_selection_ + text: "Chart View" + onClicked: { + if (!disable_chart_selection_) + { + tabLayout.tab_model_[current_]["title"] = "Chart View" + if (stack.deep > 1) + { + stack.pop() + } + stack.push(chartsLayout) + disable_chart_selection_ = true + refresh_layout(current_) + } + } + } + Button { + width: 400; height: 400 + anchors.verticalCenter: parent.verticalCenter + text: "Domain View" + onClicked: { + if (mainApplicationView.monitors == 0) + { + dialogInitMonitor.open() + } + else if (mainApplicationView.monitors == 1) + { + controller.update_available_entity_ids("Domain", "getDataDialogSourceEntityId") + open_domain_view_( + tabLayout.tab_model_[current_]["stack_id"], + entityModelFirst.get(0).id, + entityModelFirst.get(0).name) + } + else + { + domain_id_dialog.open() + } + } + } + } + } + } + + Component { + id: domainGraphLayout_component + + DomainGraphLayout + { + id: domainGraphLayout + component_id: stack.stack_id + + onUpdate_tab_name: { + tabLayout.tab_model_[current_]["title"] = new_name + + // update model to set the visual change + tab_list.model = tabLayout.tab_model_ + + // update left panel information + for (var i=0; i 0) + { + controller.domain_click(stack_layout.children[i].currentItem.entity_id) + break; + } + } + } + + Connections { + target: tabLayout + + function onInitialize_domain_view_(stack_id, entity_id, domain_id) { + if (domainGraphLayout.component_id == stack_id) + { + domainGraphLayout.entity_id = entity_id + domainGraphLayout.domain_id = domain_id + domainGraphLayout.load_model() + } + } + } + + } + } + + Connections { + target: tabLayout + + function onOpen_domain_view_(stack_id, entity_id, domain_id) { + if (stack.stack_id == stack_id) + { + if (stack.deep > 1) + { + stack.pop() + } + + stack.push(domainGraphLayout_component) + refresh_layout(current_) + initialize_domain_view_(stack_id, entity_id, domain_id) + } + } + } + } + } + } + ListView { id: tab_list anchors.top: parent.top anchors.left: parent.left width: contentWidth height: tabs_height_ - z: 100 // z is the front-back order. The tab bar must always be on top of any StackView component orientation: ListView.Horizontal model: tabLayout.tab_model_ interactive: false @@ -192,85 +335,63 @@ Item { } } + Dialog { + id: domain_id_dialog - // stack layout (where idx referred to the tab, which would contain different views) - StackLayout { - id: stack_layout - z: 1 // z is the front-back order. The tab bar must always be on top of any stackview component - width: tabLayout.width - anchors.top: tab_list.bottom; anchors.bottom: tabLayout.bottom + property bool enable_ok_button: false // disable OK button until user selects domain id - Component { - id: stack_component + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 - // view with the different views available in a tab - StackView { - id: stack - anchors.fill: parent - initialItem: view_selector + width: 300 - // override push transition to none - pushEnter: Transition {} + modal: true + title: "Select DDS Domain" - // menu that allows the selection of the view, and changes the stack if necessary - Component { - id: view_selector - Rectangle { - Row { - anchors{ - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - height: parent.height - width: childrenRect.width - spacing: 60 - Button { - width: 400; height: 400 - anchors.verticalCenter: parent.verticalCenter - enabled: !disable_chart_selection - text: "Chart View" - onClicked: { - if (!disable_chart_selection) - { - tabLayout.tab_model_[current_]["title"] = "Chart View" - if (stack.deep > 1) - { - stack.pop() - } - stack.push(chartsLayout) - disable_chart_selection = true - refresh_layout(current_) - } - } - } - Button { - width: 400; height: 400 - anchors.verticalCenter: parent.verticalCenter - text: "Domain View" - onClicked: { - tabLayout.tab_model_[current_]["title"]="Domain View" - if (stack.deep > 1) - { - stack.pop() - } - stack.push(domainViewLayout) - refresh_layout(current_) - } - } - } - } - } - } + footer: DialogButtonBox { + id: buttons + standardButtons: Dialog.Ok | Dialog.Cancel + } + + onAboutToShow: { + custom_combobox.currentIndex = -1 + controller.update_available_entity_ids("Domain", "getDataDialogSourceEntityId") + custom_combobox.recalculateWidth() + enable_ok_button = false + buttons.standardButton(Dialog.Ok).enabled = false + } + + onEnable_ok_buttonChanged: { + buttons.standardButton(Dialog.Ok).enabled = domain_id_dialog.enable_ok_button } - } - Component { - id: domainViewLayout + AdaptiveComboBox { + id: custom_combobox + textRole: "name" + valueRole: "id" + displayText: currentIndex === -1 + ? ("Please choose a Domain ID") + : ("DDS Domain " + currentText) + model: entityModelFirst - Rectangle{ - Text{ - text: "Here would be the domain view" + Component.onCompleted: + { + currentIndex = -1 + custom_combobox.recalculateWidth() } + + onActivated: { + domain_id_dialog.enable_ok_button = true + custom_combobox.recalculateWidth() + } + } + + onAccepted: + { + open_domain_view_( + tabLayout.tab_model_[current_]["stack_id"], + entityModelFirst.get(custom_combobox.currentIndex).id, + entityModelFirst.get(custom_combobox.currentIndex).name) } } @@ -278,7 +399,7 @@ Item { { var idx = tabLayout.tab_model_.length tabLayout.tab_model_[idx] = {"idx" : idx, "title": "New Tab", "stack_id":last_index_} - var new_stack = stack_component.createObject(null, {"id": last_index_, "anchors.fill": "parent"}) + var new_stack = stack_component.createObject(null, {"stack_id": tabLayout.tab_model_[idx]["stack_id"]}) last_index_++ stack_layout.children.push(new_stack) refresh_layout(idx) @@ -293,9 +414,22 @@ Item { if (idx != current_) { current_ = idx + // move to the idx tab in the stack stack_layout.currentIndex = tabLayout.tab_model_[idx]["stack_id"] - refresh_layout(current_) + + // check if domain info has changed + if (tabLayout.tab_model_[idx]["title"].includes("Domain")) + { + for (var i=0; ichange_max_points(chartbox_id, series_id, new_max_point); } + +QString Controller::get_domain_view_graph( + QString entity_id) +{ + backend::Graph domain_view = engine_->get_domain_view_graph(backend::models_id_to_backend_id(entity_id)); + return QString::fromUtf8(domain_view.dump().data(), int(domain_view.dump().size())); +} diff --git a/src/Engine.cpp b/src/Engine.cpp index 73eef257..873d9461 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -1286,6 +1286,12 @@ bool Engine::data_kind_has_target( return backend_connection_.data_kind_has_target(backend::string_to_data_kind(data_kind)); } +backend::Graph Engine::get_domain_view_graph ( + const backend::EntityId& domain_id) +{ + return backend_connection_.get_domain_view_graph(domain_id); +} + bool EntityClicked::is_set() const { return kind != backend::EntityKind::INVALID; diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index b8b7362c..87e2f2eb 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -765,6 +765,22 @@ bool SyncBackendConnection::build_source_target_entities_vectors( return two_entities_data; } +Graph SyncBackendConnection::get_domain_view_graph ( + const EntityId& domain_id) +{ + try + { + return StatisticsBackend::get_domain_view_graph(domain_id); + } + catch (const Exception& e) + { + qWarning() << "Fail getting the domain view JSON graph for entity id " + << domain_id.value() << ":" << e.what(); + static_cast(e); // In release qWarning does not compile and so e is not used + return Graph(); + } +} + void SyncBackendConnection::change_unit_magnitude( std::vector& data, DataKind data_kind)