diff --git a/CMakeLists.txt b/CMakeLists.txt index 89858132..910d7578 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -233,6 +233,8 @@ set(PROJECT_HEADERS ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/statistics/EntityItem.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/SubListedListItem.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/SubListedListModel.h + ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/tree/ProblemTreeItem.h + ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/tree/ProblemTreeModel.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/tree/TreeItem.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/tree/TreeModel.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/statistics/DataChartBox.h @@ -261,6 +263,8 @@ set(PROJECT_SOURCES_NO_MAIN ${PROJECT_SOURCE_DIR}/src/model/physical/ProcessModelItem.cpp ${PROJECT_SOURCE_DIR}/src/model/statistics/EntityItem.cpp ${PROJECT_SOURCE_DIR}/src/model/SubListedListModel.cpp + ${PROJECT_SOURCE_DIR}/src/model/tree/ProblemTreeItem.cpp + ${PROJECT_SOURCE_DIR}/src/model/tree/ProblemTreeModel.cpp ${PROJECT_SOURCE_DIR}/src/model/tree/TreeItem.cpp ${PROJECT_SOURCE_DIR}/src/model/tree/TreeModel.cpp ${PROJECT_SOURCE_DIR}/src/statistics/DataChartBox.cpp diff --git a/fastdds_monitor.pro b/fastdds_monitor.pro index f96cf855..d3771b80 100644 --- a/fastdds_monitor.pro +++ b/fastdds_monitor.pro @@ -24,6 +24,8 @@ SOURCES += \ src/model/physical/ProcessModelItem.cpp \ src/model/statistics/EntityItem.cpp \ src/model/SubListedListModel.cpp \ + src/model/tree/ProblemTreeItem.cpp \ + src/model/tree/ProblemTreeModel.cpp \ src/model/tree/TreeItem.cpp \ src/model/tree/TreeModel.cpp \ src/statistics/DataChartBox.cpp \ @@ -75,6 +77,8 @@ HEADERS += \ include/fastdds_monitor/model/statistics/EntityItem.h \ include/fastdds_monitor/model/SubListedListItem.h \ include/fastdds_monitor/model/SubListedListModel.h \ + include/fastdds_monitor/model/tree/ProblemTreeModel.h \ + include/fastdds_monitor/model/tree/ProblemTreeItem.h \ include/fastdds_monitor/model/tree/TreeItem.h \ include/fastdds_monitor/model/tree/TreeModel.h \ include/fastdds_monitor/statistics/DataChartBox.h \ diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 17f61ace..b1f5a40b 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -26,6 +26,8 @@ #include #include +#include + class Engine; enum class ErrorType : int @@ -59,6 +61,15 @@ class Controller : public QObject QString error_msg, ErrorType error_type = ErrorType::GENERIC); + //! Status counters displayed in the QML + struct StatusCounters + { + std::map errors; + std::map warnings; + uint32_t total_errors = 0; + uint32_t total_warnings = 0; + } status_counters; + public slots: // Methods to be called from QML @@ -275,6 +286,9 @@ public slots: //! Signal to inform qml that a new monitor has been initialized void monitorInitialized(); + //! Signal to notify status counters have been updated + void update_status_counters(QString errors, QString warnings); + protected: //! Reference to \c Engine object diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 129b6d95..4cdc0bd1 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include #include #include +#include struct EntityClicked { @@ -267,6 +269,13 @@ class Engine : public QQmlApplicationEngine bool new_entity = true, bool last_clicked = false); + bool update_problem( + const backend::EntityId& id, + backend::StatusKind kind); + + bool update_problem_entities( + const backend::EntityId& id); + /** * @brief Update the internal dds model with entities related with Entity referenced by \c id * @@ -348,6 +357,19 @@ class Engine : public QQmlApplicationEngine bool add_callback( backend::Callback callback); + /** + * @brief add a callback arrived from the backend to the callback queue + * + * Add a callback to the callback queue in order to process it afterwards by the main thread. + * Emit a signal that communicate the main thread that there are info to process in the callback queue. + * Add a callback issue. + * + * @param callback new callback to add + * @return true + */ + bool add_callback( + backend::ProblemCallback callback); + /** * @brief Refresh the view * @@ -390,6 +412,14 @@ class Engine : public QQmlApplicationEngine */ void process_callback_queue(); + /** + * @brief Pop problem callbacks from the callback queues while non empty and update the models + * + * @warning This method must be executed from the main Thread (or at least a QThread) so the models are + * updated in the view when modified. + */ + void process_problem_callback_queue(); + //! Refresh summary panel void refresh_summary(); @@ -507,14 +537,26 @@ class Engine : public QQmlApplicationEngine */ void new_callback_signal(); + /** + * Internal signal that communicate that there are callbacks to process by the main Thread. + * Arise from \c add_callback + */ + void new_problem_callback_signal(); + public slots: /** - * Receive the internal signal \c new_callback_signal and start the process of - * callback queue by \c process_callback_queue + * Receive the internal signal \c new_callback_signal and start the process of callback + * queue by \c process_callback_queue */ void new_callback_slot(); + /** + * Receive the internal signal \c new_problem_callback_signal and start the process of problem + * callback queue by \c process_problem_callback_queue + */ + void new_problem_callback_slot(); + protected: /** @@ -633,13 +675,23 @@ public slots: //! True if there are callbacks in the callback queue bool are_callbacks_to_process_(); + //! True if there are problem callbacks in the callback queue + bool are_problem_callbacks_to_process_(); + //! Pop a callback from callback queues and call \c read_callback for that callback bool process_callback_(); + //! Pop a problem callback from callback queues and call \c read_callback for that problem callback + bool process_problem_callback_(); + //! Update the model concerned by the entity in the callback bool read_callback_( backend::Callback callback); + //! Update the model concerned by the entity in the problem callback + bool read_callback_( + backend::ProblemCallback callback); + //! Common method to demultiplex to update functions depending on the entity kind bool update_entity_generic( backend::EntityId entity_id, @@ -695,6 +747,9 @@ public slots: //! Data that is represented in the Status Model when this model is refreshed backend::Info status_info_; + //! Data Model for Fast DDS Monitor problem view. Collects all entities problems detected by the monitor service + models::ProblemTreeModel* problem_model_; + //! TODO models::ListModel* source_entity_id_model_; @@ -722,9 +777,15 @@ public slots: //! Mutex to protect \c callback_queue_ std::recursive_mutex callback_queue_mutex_; + //! Mutex to protect \c problem_callback_queue_ + std::recursive_mutex problem_callback_queue_mutex_; + //! Queue of Callbacks that have arrived by the \c Listener and have not been processed QQueue callback_queue_; + //! Queue of Problem Callbacks that have arrived by the \c Listener and have not been processed + QQueue problem_callback_queue_; + //! Object that manage all the communications with the QML view Controller* controller_; @@ -746,6 +807,9 @@ public slots: * to happen) there are going to create entities already created. */ std::recursive_mutex initializing_monitor_; + + //! All status log + backend::Info problem_status_log_; }; #endif // _EPROSIMA_FASTDDS_MONITOR_ENGINE_H diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index 459509c4..49f2d424 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -91,6 +91,12 @@ class Listener : public PhysicalListener EntityId datawriter_id, const Status& status) override; + //! Callback when a problem is reported + void on_problem_reported( + EntityId domain_id, + EntityId entity_id, + StatusKind data_kind) override; + protected: //! Engine reference diff --git a/include/fastdds_monitor/backend/ProblemCallback.h b/include/fastdds_monitor/backend/ProblemCallback.h new file mode 100644 index 00000000..d4ca47d6 --- /dev/null +++ b/include/fastdds_monitor/backend/ProblemCallback.h @@ -0,0 +1,60 @@ +// 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 . + +/** + * @file ProblemCallback.h + */ + +#ifndef _EPROSIMA_FASTDDS_MONITOR_BACKEND_PROBLEM_CALLBACK_H +#define _EPROSIMA_FASTDDS_MONITOR_BACKEND_PROBLEM_CALLBACK_H + +#include + +namespace backend { + +/* + * Struct that store the problem callback information required by the GUI. + * It encapsulates the domain id, entity id and the kind of the new problem reported. + */ +struct ProblemCallback +{ + //! Void constructor to use copy constructor afterwards + ProblemCallback() + { + } + + //! Standard constructor with the two fields required + ProblemCallback( + backend::EntityId domain_entity_id, + backend::EntityId entity_id, + backend::StatusKind status_kind) + : domain_entity_id(domain_entity_id) + , entity_id(entity_id) + , status_kind(status_kind) + { + } + //! Information of the domain \c EntityId the callback refers + backend::EntityId domain_entity_id; + //! Information of the \c EntityId the callback refers + backend::EntityId entity_id; + //! Information of the \c StatusKind the callback refers + backend::StatusKind status_kind; +}; + +} // namespace backend + +#endif // _EPROSIMA_FASTDDS_MONITOR_BACKEND_PROBLEM_CALLBACK_H diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index ee4ab907..b48633a2 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -23,6 +23,7 @@ #define _EPROSIMA_FASTDDS_MONITOR_BACKEND_SYNCBACKENDCONNECTION_H #include +#include #include #include @@ -147,6 +148,42 @@ class SyncBackendConnection Timestamp start_time = Timestamp(), Timestamp end_time = std::chrono::system_clock::now()); + void get_status_data( + EntityId source_entity_id, + ConnectionListSample& sample); + + void get_status_data( + EntityId source_entity_id, + DeadlineMissedSample& sample); + + void get_status_data( + EntityId source_entity_id, + IncompatibleQosSample& sample); + + void get_status_data( + EntityId source_entity_id, + InconsistentTopicSample& sample); + + void get_status_data( + EntityId source_entity_id, + LivelinessChangedSample& sample); + + void get_status_data( + EntityId source_entity_id, + LivelinessLostSample& sample); + + void get_status_data( + EntityId source_entity_id, + ProxySample& sample); + + void get_status_data( + EntityId source_entity_id, + SampleLostSample& sample); + + /*void get_status_data( + EntityId source_entity_id, + StatusesSizeSample& sample);*/ + //! Get info from an entity from the Backend std::vector get_entities( EntityKind entity_type, diff --git a/include/fastdds_monitor/backend/backend_types.h b/include/fastdds_monitor/backend/backend_types.h index ec5753d8..6e2b5db8 100644 --- a/include/fastdds_monitor/backend/backend_types.h +++ b/include/fastdds_monitor/backend/backend_types.h @@ -33,15 +33,32 @@ namespace backend { using EntityId = eprosima::statistics_backend::EntityId; using EntityKind = eprosima::statistics_backend::EntityKind; using DataKind = eprosima::statistics_backend::DataKind; +using StatusKind = eprosima::statistics_backend::StatusKind; using StatisticKind = eprosima::statistics_backend::StatisticKind; using EntityInfo = eprosima::statistics_backend::Info; using Timestamp = eprosima::statistics_backend::Timestamp; +// Problem status types from backend +using ConnectionListSample = eprosima::statistics_backend::ConnectionListSample; +using DeadlineMissedSample = eprosima::statistics_backend::DeadlineMissedSample; +using IncompatibleQosSample = eprosima::statistics_backend::IncompatibleQosSample; +using InconsistentTopicSample = eprosima::statistics_backend::InconsistentTopicSample; +using LivelinessChangedSample = eprosima::statistics_backend::LivelinessChangedSample; +using LivelinessLostSample = eprosima::statistics_backend::LivelinessLostSample; +using ProxySample = eprosima::statistics_backend::ProxySample; +using SampleLostSample = eprosima::statistics_backend::SampleLostSample; +//using StatusesSizeSample = eprosima::statistics_backend::StatusesSizeSample; + //! Reference the ID_ALL in the project extern const EntityId ID_ALL; //! Reference the ID_NONE in the project extern const EntityId ID_NONE; +//! Reference for problem status (ok, error or warning) +static constexpr const char* PROBLEM_STATUS_ERROR = "error"; +static constexpr const char* PROBLEM_STATUS_OK = "ok"; +static constexpr const char* PROBLEM_STATUS_WARNING = "warning"; + } //namespace backend #endif // _EPROSIMA_FASTDDS_MONITOR_BACKEND_BACKENDTYPES_H diff --git a/include/fastdds_monitor/backend/backend_utils.h b/include/fastdds_monitor/backend/backend_utils.h index 009e6c03..25af9e1c 100644 --- a/include/fastdds_monitor/backend/backend_utils.h +++ b/include/fastdds_monitor/backend/backend_utils.h @@ -87,6 +87,10 @@ std::string data_kind_to_string( std::string statistic_kind_to_string( const StatisticKind& statistic_kind); +//! Converts the \c StatusKind to string +std::string status_kind_to_string( + const StatusKind& status_kind); + //! Retrieves the \c EntityKind related with its name in QString backend::EntityKind string_to_entity_kind( const QString& entity_kind); @@ -99,7 +103,6 @@ backend::DataKind string_to_data_kind( backend::StatisticKind string_to_statistic_kind( const QString& statistic_kind); - //! recursive function to convert array json subelements to dictionaries indexed by numbers backend::EntityInfo refactor_json( backend::EntityInfo json_data); @@ -108,6 +111,15 @@ backend::EntityInfo refactor_json( std::string timestamp_to_string( const backend::Timestamp timestamp); +std::string policy_id_to_string( + const uint32_t& id); + +std::string problem_description( + const backend::StatusKind kind); + +std::string policy_documentation_description( + const uint32_t& id); + } //namespace backend #endif // _EPROSIMA_FASTDDS_MONITOR_BACKEND_BACKENDUTILS_H diff --git a/include/fastdds_monitor/model/tree/ProblemTreeItem.h b/include/fastdds_monitor/model/tree/ProblemTreeItem.h new file mode 100644 index 00000000..17bfcc4d --- /dev/null +++ b/include/fastdds_monitor/model/tree/ProblemTreeItem.h @@ -0,0 +1,168 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Maurizio Ingrassia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// 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 . + +#ifndef _EPROSIMA_FASTDDS_MONITOR_MODEL_TREE_PROBLEMTREEITEM_H +#define _EPROSIMA_FASTDDS_MONITOR_MODEL_TREE_PROBLEMTREEITEM_H + +#include +#include + +namespace models { + +/*! + * This class represents a node of the ProblemTreeModel. + * The items are meant to be managed from the ProblemTreeModel, thus is only allowed + * to modify the stored data. + * Parenting and deletion are dealt from the ProblemTreeModel. Deleting a ProblemTreeItem + * will call the delete for each child node. + */ +class ProblemTreeItem +{ + friend class ProblemTreeModel; + +public: + + //! Create an empty item. + ProblemTreeItem(); + + //! Create an item with the given data. + explicit ProblemTreeItem( + const QVariant& data); + + //! Create an Entity item / top level item + explicit ProblemTreeItem( + const backend::EntityId& id, + const std::string& name, + const bool& is_error, + const std::string& description); + + //! Create an item with the problem parameters + explicit ProblemTreeItem( + const backend::EntityId& id, + const backend::StatusKind& kind, + const std::string& name, + const bool& is_error, + const std::string& value, + const std::string& description); + + //! Destroy the item. It will destroy every child. + ~ProblemTreeItem(); + + //! Return the stored data of the node. + const QVariant& data() const; + + //! Return the stored data of the node. + const QVariant& data( + int role) const; + + const QVariant& entity_id() const; + + const QVariant& status_kind() const; + + const QVariant& name() const; + + const QVariant& status() const; + + const QVariant& value() const; + + const QVariant& description() const; + + const QVariant& alive() const; + + //! Set the internal data of the node. + void setData( + const QVariant& data); + + //! Return the number of children nodes. + int childCount() const; + + int row() const; + + //! Return true if the node is a leaf node (no children). + bool isLeaf() const; + + //! Return the depth of this node inside the tree. + int depth() const; + + ProblemTreeItem* child( + int row); + + backend::EntityId id(); + + backend::StatusKind kind(); + + //! Increases the issues counter of a top level entity item + int recalculate_entity_counter(); + +private: + ProblemTreeItem* parentItem(); + + void setParentItem( + ProblemTreeItem* parentItem); + + void appendChild( + ProblemTreeItem* item); + + void removeChild( + ProblemTreeItem* item); + +private: + ProblemTreeItem* parent_item_; + QVector child_items_; + backend::EntityId id_; + backend::StatusKind kind_; + std::string name_; + bool is_status_error_; + std::string value_; + std::string description_; + bool is_active_; + QVariant id_variant_; + QVariant kind_variant_; + QVariant name_variant_; + QVariant is_status_error_variant_; + QVariant value_variant_; + QVariant description_variant_; + QVariant is_active_variant_; +}; + +} // namespace models + +#endif // _EPROSIMA_FASTDDS_MONITOR_MODEL_TREE_PROBLEMTREEITEM_H diff --git a/include/fastdds_monitor/model/tree/ProblemTreeModel.h b/include/fastdds_monitor/model/tree/ProblemTreeModel.h new file mode 100644 index 00000000..d97e941f --- /dev/null +++ b/include/fastdds_monitor/model/tree/ProblemTreeModel.h @@ -0,0 +1,170 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Maurizio Ingrassia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// 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 . + +#ifndef _EPROSIMA_FASTDDS_MONITOR_MODEL_TREE_PROBLEMTREEMODEL_H +#define _EPROSIMA_FASTDDS_MONITOR_MODEL_TREE_PROBLEMTREEMODEL_H + +#include + +#include + +namespace models { + +/*! + * The Tree Model works as List Model, using one column and using the row information + * referred to the parent node. + */ +class ProblemTreeModel : public QAbstractItemModel +{ + Q_OBJECT + //Q_ENUMS(models::ProblemTreeItem::ModelItemRoles) + +public: + explicit ProblemTreeModel( + QObject* parent = nullptr); + + ~ProblemTreeModel() override; + + // Overriden method from QAbstractItemModel + +public: + + //! Role names to allow queries to get some specific information from the Item + enum ModelItemRoles + { + idRole = Qt::UserRole + 1, //! Role for attribute Id + statusRole, //! Role for attribute Status + kindRole, //! Role for attribute Kind + valueRole, //! Role for attribute Value + descriptionRole, //! Role for attribute Description + aliveRole, //! Role for attribute Alive + nameRole //! Role for attribute Name + // The nameRole must always be the last one as it is used in child classes + // as the initial role of the enumeration) + }; + + int rowCount( + const QModelIndex& index) const override; + int columnCount( + const QModelIndex& index) const override; + + QModelIndex index( + int row, + int column, + const QModelIndex& parent) const override; + + QModelIndex parent( + const QModelIndex& childIndex) const override; + + QVariant data( + const QModelIndex& index, + int role = 0) const override; + + bool setData( + const QModelIndex& index, + const QVariant& value, + int role = Qt::EditRole) override; + + QHash roleNames() const override; + +public: + //! Add an item to the top level. + void addTopLevelItem( + ProblemTreeItem* child); + + //! Add a child to the parent item. + void addItem( + ProblemTreeItem* parent, + ProblemTreeItem* child); + + //! Remove the item from the model. + void removeItem( + ProblemTreeItem* item); + + //! Return the root item of the model. + ProblemTreeItem* rootItem() const; + + //! Return the depth for the given index + Q_INVOKABLE int depth( + const QModelIndex& index) const; + + //! Clear the model. + Q_INVOKABLE void clear(); + + /*! + * Return the root item to the QML Side. + * This method is not meant to be used in client code. + */ + Q_INVOKABLE QModelIndex rootIndex(); + + // Check if top level item is defined + bool containsTopLevelItem( + ProblemTreeItem* child); + + // Check if item is defined in the parent item + bool contains( + ProblemTreeItem* parent, + ProblemTreeItem* child); + + // Check if default empty value is the only element + bool is_empty(); + + void removeEmptyItem(); + + //! Looks for a TopLevelItem that matches that id. If not existing, creates new one and returns it + ProblemTreeItem* getTopLevelItem( + const backend::EntityId& id, + const std::string& data, + const bool& is_error, + const std::string& description); + +private: + ProblemTreeItem* internalPointer( + const QModelIndex& index) const; + +private: + ProblemTreeItem* root_item_; + bool is_empty_; +}; + +} // namespace models + +#endif // _EPROSIMA_FASTDDS_MONITOR_MODEL_TREE_PROBLEMTREEMODEL_H diff --git a/qml.qrc b/qml.qrc index 22715c9e..28a42166 100644 --- a/qml.qrc +++ b/qml.qrc @@ -53,6 +53,8 @@ qml/MonitorToolBarButton.qml qml/Panels.qml qml/PhysicalView.qml + qml/ProblemTreeView.qml + qml/ProblemTreeViewItem.qml qml/QosView.qml qml/ScheduleClearDialog.qml qml/SeriesSetMaxPointsDialog.qml @@ -60,9 +62,11 @@ qml/StatisticsChartBox.qml qml/StatisticsChartView.qml qml/StatusPanel.qml + qml/StatusLayout.qml qml/StatusView.qml qml/SummaryView.qml qml/TabLayout.qml + qml/TopicMenu.qml qtquickcontrols2.conf @@ -70,6 +74,8 @@ resources/images/eprosima_logo.ico resources/images/fastdds-monitor-logo.png resources/images/graphs.svg + resources/images/domain_graph.svg + resources/images/topic_graph.svg resources/images/icons/clearissues/clearissues_black.svg @@ -80,10 +86,15 @@ resources/images/icons/clearlog/clearlog_eProsimaLightBlue.svg resources/images/icons/clearlog/clearlog_grey.svg resources/images/icons/clearlog/clearlog_white.svg + resources/images/icons/collapse/collapse_black.svg + resources/images/icons/collapse/collapse_eProsimaLightBlue.svg + resources/images/icons/collapse/collapse_grey.svg + resources/images/icons/collapse/collapse_white.svg resources/images/icons/cross/cross_black.svg resources/images/icons/cross/cross_eProsimaLightBlue.svg resources/images/icons/cross/cross_grey.svg resources/images/icons/cross/cross_white.svg + resources/images/icons/cross/cross_red.svg resources/images/icons/datareader/datareader_black.svg resources/images/icons/datareader/datareader_eProsimaLightBlue.svg resources/images/icons/datareader/datareader_grey.svg @@ -104,10 +115,27 @@ resources/images/icons/editaxis/editaxis_eProsimaLightBlue.svg resources/images/icons/editaxis/editaxis_grey.svg resources/images/icons/editaxis/editaxis_white.svg + resources/images/icons/error/error_black.svg + resources/images/icons/error/error_eProsimaLightBlue.svg + resources/images/icons/error/error_grey.svg + resources/images/icons/error/error_white.svg + resources/images/icons/error/error_red.svg + resources/images/icons/expand/expand_black.svg + resources/images/icons/expand/expand_eProsimaLightBlue.svg + resources/images/icons/expand/expand_grey.svg + resources/images/icons/expand/expand_white.svg resources/images/icons/explorer/explorer_black.svg resources/images/icons/explorer/explorer_eProsimaLightBlue.svg resources/images/icons/explorer/explorer_grey.svg resources/images/icons/explorer/explorer_white.svg + resources/images/icons/filter_empty/filter_empty_black.svg + resources/images/icons/filter_empty/filter_empty_eProsimaLightBlue.svg + resources/images/icons/filter_empty/filter_empty_grey.svg + resources/images/icons/filter_empty/filter_empty_white.svg + resources/images/icons/filter_full/filter_full_black.svg + resources/images/icons/filter_full/filter_full_eProsimaLightBlue.svg + resources/images/icons/filter_full/filter_full_grey.svg + resources/images/icons/filter_full/filter_full_white.svg resources/images/icons/grid1/grid1_black.svg resources/images/icons/grid1/grid1_eProsimaLightBlue.svg resources/images/icons/grid1/grid1_grey.svg @@ -195,5 +223,21 @@ resources/images/icons/help/help_eProsimaLightBlue.svg resources/images/icons/help/help_grey.svg resources/images/icons/help/help_white.svg + resources/images/icons/left_arrow/left_arrow_black.svg + resources/images/icons/left_arrow/left_arrow_eProsimaLightBlue.svg + resources/images/icons/left_arrow/left_arrow_grey.svg + resources/images/icons/left_arrow/left_arrow_white.svg + resources/images/icons/right_arrow/right_arrow_black.svg + resources/images/icons/right_arrow/right_arrow_eProsimaLightBlue.svg + resources/images/icons/right_arrow/right_arrow_grey.svg + resources/images/icons/right_arrow/right_arrow_white.svg + resources/images/icons/rounded_left_arrow/rounded_left_arrow_black.svg + resources/images/icons/rounded_left_arrow/rounded_left_arrow_eProsimaLightBlue.svg + resources/images/icons/rounded_left_arrow/rounded_left_arrow_grey.svg + resources/images/icons/rounded_left_arrow/rounded_left_arrow_white.svg + resources/images/icons/rounded_right_arrow/rounded_right_arrow_black.svg + resources/images/icons/rounded_right_arrow/rounded_right_arrow_eProsimaLightBlue.svg + resources/images/icons/rounded_right_arrow/rounded_right_arrow_grey.svg + resources/images/icons/rounded_right_arrow/rounded_right_arrow_white.svg diff --git a/qml/ChangeAliasDialog.qml b/qml/ChangeAliasDialog.qml index 3c0507c0..fc16d65e 100644 --- a/qml/ChangeAliasDialog.qml +++ b/qml/ChangeAliasDialog.qml @@ -32,6 +32,7 @@ Dialog { standardButtons: Dialog.Ok | Dialog.Cancel anchors.centerIn: Overlay.overlay + property var domainEntityId: "" property var entityId: "" property var currentAlias: "" property var entityKind: "" @@ -79,6 +80,7 @@ Dialog { } else { controller.set_alias(entityId, newSeriesNameTextField.text, entityKind) } + refreshDomainGraphView(domainEntityId, entityId) } } } diff --git a/qml/DomainGraphLayout.qml b/qml/DomainGraphLayout.qml index e0eb7e5b..3faad24d 100644 --- a/qml/DomainGraphLayout.qml +++ b/qml/DomainGraphLayout.qml @@ -33,6 +33,8 @@ Item // Public signals signal update_tab_name(string new_name) // Update tab name based on selected domain id + signal openEntitiesMenu(string domainEntityId, string entityId, string currentAlias, string entityKind) + signal openTopicMenu(string domainEntityId, string domainId, string entityId, string currentAlias, string entityKind) // Private properties property var topic_locations_: {} // topic information needed for connection representation @@ -79,6 +81,7 @@ Item // Obtain given domain id graph Component.onCompleted: { + filtered_topics_ = [] load_model() } @@ -233,9 +236,14 @@ Item MouseArea { anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { - controller.topic_click(modelData["id"]) + if(mouse.button & Qt.RightButton) { + openTopicMenu(entity_id, domain_id, modelData["id"], modelData["alias"], modelData["kind"]) + } else { + controller.topic_click(modelData["id"]) + } } } } @@ -513,8 +521,8 @@ Item } IconSVG { visible: modelData["status"] != "OK" - name: "issues" - color: "white" + name: modelData["status"] == "WARNING" ? "issues" : "error" + color: modelData["status"] == "WARNING" ? "white" : "red" size: modelData["status"] != "OK"? icon_size_ : 0 } Rectangle { @@ -535,9 +543,14 @@ Item MouseArea { anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { - controller.host_click(modelData["id"]) + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(entity_id, modelData["id"], modelData["alias"], modelData["kind"]) + } else { + controller.host_click(modelData["id"]) + } } } } @@ -635,8 +648,8 @@ Item } IconSVG { visible: modelData["status"] != "OK" - name: "issues" - color: "white" + name: modelData["status"] == "WARNING" ? "issues" : "error" + color: modelData["status"] == "WARNING" ? "white" : "red" size: modelData["status"] != "OK"? icon_size_ : 0 } Rectangle { @@ -657,9 +670,14 @@ Item MouseArea { anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { - controller.user_click(modelData["id"]) + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(entity_id, modelData["id"], modelData["alias"], modelData["kind"]) + } else { + controller.user_click(modelData["id"]) + } } } } @@ -754,8 +772,9 @@ Item } IconSVG { visible: modelData["status"] != "OK" - name: "issues" - color: "white" + name: modelData["status"] == "WARNING" ? "issues" : "error" + color: modelData["status"] == "WARNING" ? "white" : "red" + size: modelData["status"] != "OK"? icon_size_ : 0 } Rectangle { @@ -776,9 +795,14 @@ Item MouseArea { anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { - controller.process_click(modelData["id"]) + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(entity_id, modelData["id"], modelData["alias"], modelData["kind"]) + } else { + controller.process_click(modelData["id"]) + } } } } @@ -873,7 +897,8 @@ Item } IconSVG { visible: modelData["status"] != "OK" - name: "issues" + name: modelData["status"] == "WARNING" ? "issues" : "error" + color: modelData["status"] == "WARNING" ? "white" : "red" size: modelData["status"] != "OK"? icon_size_ : 0 } Rectangle { @@ -881,7 +906,7 @@ Item width: first_indentation_ /2 } IconSVG { - name: modelData["kind"] + name: "participant" size: icon_size_ } Label { @@ -892,9 +917,15 @@ Item MouseArea { anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { - controller.participant_click(modelData["id"]) + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(entity_id, modelData["id"], modelData["alias"], modelData["kind"]) + } else { + controller.participant_click(modelData["id"]) + } + } } } @@ -971,8 +1002,8 @@ Item var globalCoordinates = endpointComponent.mapToItem(mainSpace, 0, 0) var src_x = globalCoordinates.x + endpointComponent.width var src_y = modelData["accum_y"] + (endpointComponent.height / 2) - var left_direction = modelData["kind"] == "datareader" - var right_direction = modelData["kind"] == "datawriter" + var left_direction = modelData["kind"] == "DataReader" + var right_direction = modelData["kind"] == "DataWriter" endpoint_topic_connections_[modelData["id"]] = { "id": modelData["id"], "left_direction": left_direction, @@ -987,7 +1018,7 @@ Item id: endpoint_background width: parent.width height: endpoint_height_ - color: modelData["kind"] == "datareader" ? reader_color_ : writer_color_ + color: modelData["kind"] == "DataReader" ? reader_color_ : writer_color_ radius: radius_ } @@ -1016,7 +1047,8 @@ Item } IconSVG { visible: modelData["status"] != "OK" - name: "issues" + name: modelData["status"] == "WARNING" ? "issues" : "error" + color: modelData["status"] == "WARNING" ? "white" : "red" size: modelData["status"] != "OK"? icon_size_ : 0 } Rectangle { @@ -1024,7 +1056,8 @@ Item width: first_indentation_ /2 } IconSVG { - name: modelData["kind"] + name: modelData["kind"] == "DataReader" ++ ? "datareader" : "datawriter" size: icon_size_ } Label { @@ -1035,9 +1068,14 @@ Item MouseArea { anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { - controller.endpoint_click(modelData["id"]) + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(entity_id, modelData["id"], modelData["alias"], modelData["kind"]) + } else { + controller.endpoint_click(modelData["id"]) + } } } } @@ -1158,6 +1196,19 @@ Item // Obtain given domain id graph JSON model function load_model() { + filter_model_by_topic ("") + } + + // Filter model by topic + function filter_model_by_topic (topic_id) + { + // topic id management + var topic_names = [] + if (topic_id != "" && !filtered_topics_.includes(topic_id)) + { + filtered_topics_[filtered_topics_.length] = topic_id; + } + // clear internal models clear_graph() @@ -1185,16 +1236,36 @@ Item 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"] + if (filtered_topics_.length > 0) + { + for (var i = 0; i < filtered_topics_.length; i++) + { + if (filtered_topics_[i] == topic) + { + topic_names[i] = new_model["topics"][topic]["alias"] + new_topics[new_topics.length] = { + "id":topic, + "kind":"Topic", + "alias":new_model["topics"][topic]["alias"] + } + } + } + } + else + { + new_topics[new_topics.length] = { + "id":topic, + "kind":"Topic", + "alias":new_model["topics"][topic]["alias"] + } } } } var accum_y = 0 + var temp_y = 0 for (var host in new_model["hosts"]) { + var discard_host = true var metatraffic_ = new_model["hosts"][host]["metatraffic"] if (metatraffic_ != true || is_metatraffic_visible_) { @@ -1226,20 +1297,30 @@ Item 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 + if ((!filtered_topics_.length) || (filtered_topics_.length > 0 + && filtered_topics_.includes(new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["topic"]))) + { + discard_host = false + var kind = "DataWriter" + if (new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["kind"] == "datareader") + { + kind = "DataReader" + } + new_endpoints[new_endpoints.length] = { + "id":endpoint, + "kind":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_ } - accum_y += endpoint_height_ + elements_spacing_ } } new_participants[new_participants.length] = { "id":participant, - "kind":new_model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["kind"], + "kind": "DomainParticipant", "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"], @@ -1251,7 +1332,7 @@ Item } new_processes[new_processes.length] = { "id":process, - "kind":new_model["hosts"][host]["users"][user]["processes"][process]["kind"], + "kind":"Process", "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"], @@ -1262,7 +1343,7 @@ Item } new_users[new_users.length] = { "id":user, - "kind":new_model["hosts"][host]["users"][user]["kind"], + "kind": "User", "alias":new_model["hosts"][host]["users"][user]["alias"], "status":new_model["hosts"][host]["users"][user]["status"], "processes":new_processes @@ -1270,15 +1351,23 @@ Item 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 + if (!discard_host) + { + new_hosts[new_hosts.length] = { + "id":host, + "kind":"Host", + "alias":new_model["hosts"][host]["alias"], + "status":new_model["hosts"][host]["status"], + "users":new_users + } + accum_y += elements_spacing_ + temp_y = accum_y + } + else { + accum_y = temp_y } - accum_y += elements_spacing_ } + } model = { "kind": new_model["kind"], @@ -1310,7 +1399,31 @@ Item } // Update tab name with selected domain id - domainGraphLayout.update_tab_name("Domain " + domain_id + " View") + if (filtered_topics_.length > 0) + { + if (filtered_topics_.length == 1) + { + domainGraphLayout.update_tab_name(topic_names[0] + " Topic View") + } + else + { + var print_topic_names = topic_names[0] + for (var i = 1; i < topic_names.length -1; i++) + { + print_topic_names += ", " + topic_names[i] + } + if (print_topic_names.length-1 > 0) + { + print_topic_names += " and " + topic_names[topic_names.length-1] + } + + domainGraphLayout.update_tab_name(print_topic_names + " Topics View") + } + } + else + { + domainGraphLayout.update_tab_name("Domain " + domain_id + " View") + } } // remove drawn connections @@ -1339,4 +1452,87 @@ Item } } } + + // check if model contains entity + function contains_entity(domainEntityId, entityId) + { + // check if domainEntityId has content + if (domainEntityId != "") + { + // belongs to the current domain + if (entity_id.toString() != domainEntityId) + { + return false + } + } + // check all entities by entityId + + // check domain + if(entity_id.toString() == entityId) + { + return true + } + + // check topics + for (var topic in model["topics"]) + { + if (model["topics"][topic]["id"] == entityId) + { + return true + } + } + + // check entities + for (var host in model["hosts"]) + { + if (model["hosts"][host]["id"] == entityId) + { + return true + } + else + { + for (var user in model["hosts"][host]["users"]) + { + if (model["hosts"][host]["users"][user]["id"] == entityId) + { + return true + } + else + { + for (var process in model["hosts"][host]["users"][user]["processes"]) + { + if (model["hosts"][host]["users"][user]["processes"][process]["id"] == entityId) + { + return true + } + else + { + for (var participant in model["hosts"][host]["users"][user]["processes"][process]["participants"]) + { + if (model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["id"] == entityId) + { + return true + } + else + { + for (var endpoint in model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"]) + { + if (model["hosts"][host]["users"][user]["processes"][process]["participants"][participant]["endpoints"][endpoint]["id"] == entityId) + { + return true + } + } + } + } + } + } + } + } + } + } + + + // not found yet + return false + } } diff --git a/qml/EntitiesMenu.qml b/qml/EntitiesMenu.qml index 48b084f6..d43fcf69 100644 --- a/qml/EntitiesMenu.qml +++ b/qml/EntitiesMenu.qml @@ -7,12 +7,17 @@ import Theme 1.0 */ Menu { id: entitiesMenu + property string domainEntityId: "" property string entityId: "" property string currentAlias: "" property string entityKind: "" MenuItem { text: "Change alias" - onTriggered: changeAlias(menu.entityId, menu.currentAlias, menu.entityKind) + onTriggered: changeAlias(menu.domainEntityId, menu.entityId, menu.currentAlias, menu.entityKind) + } + MenuItem { + text: "View Problems" + onTriggered: filterProblemLog(menu.entityId) } } diff --git a/qml/EntityList.qml b/qml/EntityList.qml index 0746d97b..61c534d2 100644 --- a/qml/EntityList.qml +++ b/qml/EntityList.qml @@ -90,7 +90,7 @@ Rectangle { } onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openEntitiesMenu("", id, name, kind) } else { controller.participant_click(id) } @@ -173,7 +173,7 @@ Rectangle { } onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openEntitiesMenu("", id, name, kind) } else { controller.endpoint_click(id) } @@ -245,7 +245,7 @@ Rectangle { onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openEntitiesMenu("", id, name, kind) } else { controller.locator_click(id) } diff --git a/qml/GraphConnection.qml b/qml/GraphConnection.qml index 5464d3fc..e6a2c211 100644 --- a/qml/GraphConnection.qml +++ b/qml/GraphConnection.qml @@ -20,17 +20,16 @@ Item { 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 + color: "white" } Rectangle { id: left_background - opacity: 0.70 anchors.top: parent.top; anchors.bottom: parent.bottom - anchors.topMargin: -2; anchors.bottomMargin: -2 + anchors.topMargin: arrow_margin_; anchors.bottomMargin: arrow_margin_ anchors.left: parent.left; anchors.right: parent.right anchors.leftMargin: parent.height /2; anchors.rightMargin: 5; - color: background_color + color: "white" } @@ -39,34 +38,26 @@ Item { Item { id: left_arrow_background visible: left_direction - height: arrow_size_ + 8 - width: arrow_size_ + 2 - opacity: 0.7 + height: arrow_size_ + 20 + width: arrow_size_ + clip: true 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') + anchors.left: parent.left; anchors.leftMargin: -4 + + IconSVG { + anchors.left: parent.left + anchors.top: parent.top + name: "left_arrow" + color: "white" + size: arrow_size_ + 10 + } - 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() - } + IconSVG { + anchors.left: parent.left + anchors.bottom: parent.bottom + name: "left_arrow" + color: "white" + size: arrow_size_ + 10 } } @@ -77,29 +68,12 @@ Item { height: arrow_size_ width: arrow_size_ anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - - Canvas { - id: left_canvas - - anchors.centerIn: parent + anchors.left: parent.left; anchors.leftMargin: -5 - 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() - } + IconSVG { + name: "left_arrow" + color: "grey" + size: arrow_size_ } } @@ -108,8 +82,8 @@ Item { 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 + anchors.leftMargin: left_direction ? 10 : 0 + anchors.rightMargin: right_direction ? 15 : 0 color: arrow_color } @@ -117,32 +91,15 @@ Item { Item { id: right_arrow visible: right_direction - height: arrow_size_ - width: arrow_size_ + height: arrow_size_ -4 + width: arrow_size_ -4 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() - } + IconSVG { + name: "right_arrow" + color: "grey" + size: arrow_size_-5 } } } diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index 4f3fe822..5247a047 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -40,6 +40,9 @@ RowLayout { signal explorerPhysicalChanged(bool status) signal explorerLogicalChanged(bool status) signal explorerEntityInfoChanged(bool status) + signal open_topic_view(string domainEntityId, string domainId, string entityId) + signal refresh_domain_graph_view(string domainEntityId, string entityId) + signal filter_problem_log(string entityId) MonitoringPanel { id: monitoringPanel @@ -65,26 +68,52 @@ RowLayout { id: entitiesMenu } + TopicMenu { + id: topicMenu + } + IssuesPanel { id: issuesPanel Layout.fillHeight: true visible: (visiblePanel === panelItem[LeftPanel.LeftSubPanel.Issues]) ? true : false } - function changeAlias(entityId, currentAlias, entityKind) { + function changeAlias(domainEntityId, entityId, currentAlias, entityKind) { + aliasDialog.domainEntityId = domainEntityId aliasDialog.entityId = entityId aliasDialog.currentAlias = currentAlias aliasDialog.entityKind = entityKind aliasDialog.open() } - function openEntitiesMenu(entityId, currentAlias, entityKind) { + function openEntitiesMenu(domainEntityId, entityId, currentAlias, entityKind) { + entitiesMenu.domainEntityId = domainEntityId entitiesMenu.entityId = entityId entitiesMenu.currentAlias = currentAlias entitiesMenu.entityKind = entityKind entitiesMenu.popup() } + function openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind) { + topicMenu.domainEntityId = domainEntityId + topicMenu.domainId = domainId + topicMenu.entityId = entityId + topicMenu.currentAlias = currentAlias + topicMenu.entityKind = entityKind + topicMenu.popup() + } + + function openTopicView(domainEntityId, domainId, entityId) { + leftPanel.open_topic_view(domainEntityId, domainId, entityId) + } + + function refreshDomainGraphView(domainEntityId, entityId) { + leftPanel.refresh_domain_graph_view(domainEntityId, entityId) + } + function filterProblemLog(entityId){ + leftPanel.filter_problem_log(entityId) + } + function expandAll(view, model) { for(var i=0; i < model.rowCount(); i++) { var index = model.index(i, 0) diff --git a/qml/LogicalView.qml b/qml/LogicalView.qml index cbbfa464..999950e4 100644 --- a/qml/LogicalView.qml +++ b/qml/LogicalView.qml @@ -63,6 +63,7 @@ Rectangle { height: domainListColumn.childrenRect.height property var domainId: id + property var domainName: name property int domainIdx: index property var topicList: topicList @@ -96,7 +97,7 @@ Rectangle { } onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openEntitiesMenu(domainId, id, name, kind) } else { controller.domain_click(id) } @@ -166,7 +167,7 @@ Rectangle { onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openTopicMenu(domainId, domainName, id, name, kind) } else { controller.topic_click(id) } diff --git a/qml/Panels.qml b/qml/Panels.qml index 84df2717..bd2a1a53 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -30,6 +30,9 @@ RowLayout { // If is set to true, the left sidebar was opened, and false if it was closed. property bool prevFullScreenLeftSidebarState: true + // private properties + property int status_layout_height: status_layout_min_height_ + signal openCloseLeftSideBar signal changeChartboxLayout(int chartsPerRow) signal explorerDDSEntitiesChanged(bool status) @@ -37,6 +40,10 @@ RowLayout { signal explorerLogicalChanged(bool status) signal explorerEntityInfoChanged(bool status) + // Read only design properties + readonly property int status_layout_min_height_: 24 + + onOpenCloseLeftSideBar: { if (panels.showLeftSidebar) { iconsVBar.iconClicked(iconsVBar.selected) @@ -90,24 +97,66 @@ RowLayout { onExplorerPhysicalChanged: panels.explorerPhysicalChanged(status) onExplorerLogicalChanged: panels.explorerLogicalChanged(status) onExplorerEntityInfoChanged: panels.explorerEntityInfoChanged(status) + onOpen_topic_view: tabs.open_topic_view(domainEntityId, domainId, entityId) + onRefresh_domain_graph_view: tabs.refresh_domain_graph_view(domainEntityId, entityId) + onFilter_problem_log: statusLayout.filter_problem_log(entityId) } - TabLayout { - id: tabs + Rectangle { SplitView.fillWidth: true - clip: true - onFullScreenChanged: { - if (fullScreen) { - if (showLeftSidebar) { - openCloseLeftSideBar() - prevFullScreenLeftSidebarState = true - } else { - prevFullScreenLeftSidebarState = false + SplitView { + id: tabsSplitView + anchors.fill: parent + orientation: Qt.Vertical + + TabLayout { + id: tabs + SplitView.fillWidth: true + SplitView.fillHeight: true + SplitView.preferredHeight: parent.height - parent.height / 5 + clip: true + + onFullScreenChanged: { + if (fullScreen) { + if (showLeftSidebar) { + openCloseLeftSideBar() + prevFullScreenLeftSidebarState = true + } else { + prevFullScreenLeftSidebarState = false + } + } else { + if (!showLeftSidebar && prevFullScreenLeftSidebarState) { + openCloseLeftSideBar() + } + } } - } else { - if (!showLeftSidebar && prevFullScreenLeftSidebarState) { - openCloseLeftSideBar() + onOpenEntitiesMenu: { + panels.openEntitiesMenu(domainEntityId, entityId, currentAlias, entityKind) + } + onOpenTopicMenu: { + panels.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind) + } + } + StatusLayout { + id: statusLayout + SplitView.preferredHeight: status_layout_height + SplitView.minimumHeight: status_layout_min_height_ + clip: true + current_status: StatusLayout.Status.Collapsed + footer_height: status_layout_min_height_ + + onClose_status_layout: { + status_layout_height = status_layout_min_height_ + statusLayout.current_status = StatusLayout.Status.Closed + } + onCollapse_status_layout: { + status_layout_height = tabsSplitView.height / 5 + statusLayout.current_status = StatusLayout.Status.Collapsed + } + onExpand_status_layout: { + status_layout_height = tabsSplitView.height + statusLayout.current_status = StatusLayout.Status.Expanded } } } @@ -145,4 +194,12 @@ RowLayout { function changeExplorerEntityInfo(status) { leftPanel.changeExplorerEntityInfo(status) } + + function openEntitiesMenu(domainEntityId, entityId, currentAlias, entityKind) { + leftPanel.openEntitiesMenu(domainEntityId, entityId, currentAlias, entityKind) + } + + function openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind) { + leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind) + } } diff --git a/qml/PhysicalView.qml b/qml/PhysicalView.qml index 0fa8cb55..e698b445 100644 --- a/qml/PhysicalView.qml +++ b/qml/PhysicalView.qml @@ -90,7 +90,7 @@ Rectangle { } onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openEntitiesMenu("", id, name, kind) } else { controller.host_click(id) } @@ -174,7 +174,7 @@ Rectangle { } onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openEntitiesMenu("", id, name, kind) } else { controller.user_click(id) } @@ -246,7 +246,7 @@ Rectangle { onClicked: { if(mouse.button & Qt.RightButton) { - openEntitiesMenu(id, name, kind) + openEntitiesMenu("", id, name, kind) } else { controller.process_click(id) } diff --git a/qml/ProblemTreeView.qml b/qml/ProblemTreeView.qml new file mode 100644 index 00000000..581c75b7 --- /dev/null +++ b/qml/ProblemTreeView.qml @@ -0,0 +1,175 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Maurizio Ingrassia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// 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.Window 2.15 +import QtQuick.Controls 2.15 +import QtQml 2.15 + +Flickable { + id: root + + implicitWidth: 400 + implicitHeight: 400 + clip: true + + property var model + readonly property alias currentIndex: tree.selectedIndex + readonly property alias currentItem: tree.currentItem + property var currentData + + property alias handle: tree.handle + property alias contentItem: tree.contentItem + property Component highlight: Rectangle { + color: root.selectedColor + } + + property alias selectionEnabled: tree.selectionEnabled + property alias hoverEnabled: tree.hoverEnabled + + property alias color: tree.color + property alias handleColor: tree.handleColor + property alias hoverColor: tree.hoverColor + property alias selectedColor: tree.selectedColor + property alias selectedItemColor: tree.selectedItemColor + + property alias rowHeight: tree.rowHeight + property alias rowPadding: tree.rowPadding + property alias rowSpacing: tree.rowSpacing + + property alias fontMetrics: tree.fontMetrics + property alias font: tree.font + + enum Handle { + Triangle, + TriangleSmall, + TriangleOutline, + TriangleSmallOutline, + Chevron, + Arrow + } + + signal problem_focused() + signal clean_filter() + + property int handleStyle: ProblemTreeView.Handle.TriangleSmallOutline + + contentHeight: tree.height + contentWidth: width + boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar {} + + Connections { function onCurrentIndexChanged() { if(currentIndex) currentData = model.data(currentIndex) } } + + ProblemTreeViewItem { + id: tree + + model: root.model + parentIndex: model.rootIndex() + childCount: model.rowCount(parentIndex) + + itemLeftPadding: 0 + color: root.color + handleColor: root.handleColor + hoverColor: root.hoverColor + selectedColor: root.selectedColor + selectedItemColor: root.selectedItemColor + defaultIndicator: indicatorToString(handleStyle) + z: 1 + + onToggled: { + root.clean_filter() + } + + Connections { + target: root.model + ignoreUnknownSignals: true + function onLayoutChanged() { + tree.childCount = root.model ? root.model.rowCount(tree.parentIndex) : 0 + } + + + } + } + + Loader { + id: highlightLoader + sourceComponent: highlight + + width: root.width + height: root.rowHeight + z: 0 + visible: root.selectionEnabled && tree.currentItem !== null + + Binding { + target: highlightLoader.item + property: "y" + value: tree.currentItem ? tree.currentItem.mapToItem(tree, 0, 0).y + tree.anchors.topMargin : 0 + when: highlightLoader.status === Loader.Ready + } + } + + function indicatorToString(handle){ + switch (handle){ + case ProblemTreeView.Handle.Triangle: return "▶"; + case ProblemTreeView.Handle.TriangleSmall: return "►"; + case ProblemTreeView.Handle.TriangleOutline: return "▷"; + case ProblemTreeView.Handle.TriangleSmallOutline: return "⊳"; + case ProblemTreeView.Handle.Chevron: return "❱"; + case ProblemTreeView.Handle.Arrow: return "➤"; + default: return "▶"; + } + } + + function focus_entity(entityId) { + var found = false + for (var i = 0; i< model.rowCount(); i++){ + if (model.data(model.index(i,0),ProblemTreeViewItem.Role.Id) == entityId) { + tree.focus(entityId) + found = true + } + } + if (found){ + root.problem_focused() + } + } +} diff --git a/qml/ProblemTreeViewItem.qml b/qml/ProblemTreeViewItem.qml new file mode 100644 index 00000000..beea7514 --- /dev/null +++ b/qml/ProblemTreeViewItem.qml @@ -0,0 +1,406 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Maurizio Ingrassia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// 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: root + + // model properties + + property var model + property var parentIndex + property var childCount + + property var currentItem: null + property var selectedIndex: null + property var hoveredIndex: null + + // layout properties + + property bool selectionEnabled: false + property bool hoverEnabled: false + + property int itemLeftPadding: 0 + property int rowHeight: 24 + property int rowPadding: 30 + property int rowSpacing: 6 + + property color color: "black" + property color inactive: "#808080" + property color handleColor: color + property color hoverColor: "lightgray" + property color selectedColor: "silver" + property color selectedItemColor: color + + property string defaultIndicator: "▶" + property FontMetrics fontMetrics: FontMetrics { + font.pointSize: Theme.font.pointSize + } + property alias font: root.fontMetrics.font + enum Role { Id=257, Status, Kind, Value, Description, Alive, Name } + + // public signal + signal toggled() + + // private (internal) signals + signal focus_(int entityId) + + implicitWidth: parent.width + implicitHeight: childrenRect.height + + // Components + + property Component handle: Rectangle { + id: handle + + implicitWidth: 20 + implicitHeight: 20 + Layout.leftMargin: parent.spacing + rotation: currentRow.expanded ? 90 : 0 + opacity: currentRow.hasChildren + color: "transparent" + + Text { + anchors.centerIn: parent + text: defaultIndicator + font: root.font + antialiasing: true + color: currentRow.isSelectedIndex ? root.selectedItemColor : root.handleColor + } + } + + property Component contentItem: Item { + id: contentData + + + IconSVG { + id: status_icon + visible: !(currentRow.currentId === "all" && currentRow.currentKind === "INVALID") + anchors.left: parent.left; anchors.leftMargin: -5 + anchors.verticalCenter: parent.verticalCenter + name: currentRow.currentStatus ? "error" :"issues" + color: currentRow.currentAlive ? currentRow.currentStatus ? "red" :"black" : "grey" + size: 15 + } + + Text { + id: entity_name + anchors.left: status_icon.right; anchors.leftMargin: 5 + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + + color:currentRow.currentAlive ? currentRow.isSelectedIndex ? root.selectedItemColor : root.color : root.inactive + text: currentRow.currentData + font: root.font + } + + Text { + id: id_value + anchors.left: entity_name.right; anchors.leftMargin: 10 + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + + color:currentRow.currentAlive ? currentRow.isSelectedIndex ? root.selectedItemColor : root.color : root.inactive + text: currentRow.currentId == undefined || currentRow.currentValue == undefined ? "" : + currentRow.currentId != "all" && currentRow.currentKind === "INVALID" ? + currentRow.currentId === "all" ? "" : "(" + currentRow.currentValue + ")" : currentRow.currentValue + font: root.font + } + + Text { + id: description + anchors.left: id_value.right; anchors.leftMargin: 20 + anchors.right: parent.right; anchors.rightMargin: 10 + anchors.verticalCenter: parent.verticalCenter + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + height: parent.height + elide: Text.ElideRight + + color:currentRow.currentAlive ? currentRow.isSelectedIndex ? root.selectedItemColor : root.color : root.inactive + text: currentRow.currentId != "all" && currentRow.currentKind === "INVALID" ? + currentRow.currentId === "all" ? "" : + "Entity ID: " + currentRow.currentId : currentRow.currentDescription + font.pointSize: Theme.font.pointSize + font.italic: true + onLinkActivated: Qt.openUrlExternally(link) + + MouseArea { + visible: currentRow.currentDescription.includes("href") + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + } + } + } + + property Component hoverComponent: Rectangle { + width: parent.width + height: parent.height + color: currentRow.isHoveredIndex && !currentRow.isSelectedIndex ? root.hoverColor : "transparent" + } + + // Body + + ColumnLayout { + width: parent.width + spacing: 0 + + Repeater { + id: repeater + model: childCount + Layout.fillWidth: true + + delegate: ColumnLayout { + id: itemColumn + + Layout.leftMargin: itemLeftPadding + spacing: 0 + + QtObject { + id: _prop + + property var currentIndex: root.model.index(index, 0, parentIndex) + property var currentData: root.model.data(currentIndex) + property var currentId: root.model.data(currentIndex, ProblemTreeViewItem.Role.Id) + property var currentStatus: root.model.data(currentIndex, ProblemTreeViewItem.Role.Status) + property var currentKind: root.model.data(currentIndex, ProblemTreeViewItem.Role.Kind) + property var currentValue: root.model.data(currentIndex, ProblemTreeViewItem.Role.Value) + property var currentDescription: root.model.data(currentIndex, ProblemTreeViewItem.Role.Description) + property var currentAlive: root.model.data(currentIndex, ProblemTreeViewItem.Role.Alive) + property Item currentItem: repeater.itemAt(index) + property bool expanded: false + property bool selected: false + property int itemChildCount: root.model.rowCount(currentIndex) + readonly property int depth: root.model.depth(currentIndex) + readonly property bool hasChildren: itemChildCount > 0 + readonly property bool isSelectedIndex: root.selectionEnabled && currentIndex === root.selectedIndex + readonly property bool isHoveredIndex: root.hoverEnabled && currentIndex === root.hoveredIndex + readonly property bool isSelectedAndHoveredIndex: hoverEnabled && selectionEnabled && isHoveredIndex && isSelectedIndex + + function toggle(){ + if(_prop.hasChildren) + { + _prop.expanded = !_prop.expanded + } + root.toggled() + } + } + + Connections { + target: root.model + ignoreUnknownSignals: true + function onLayoutChanged() { + const parent = root.model.index(index, 0, parentIndex) + _prop.itemChildCount = root.model.rowCount(parent) + // refresh counter + var new_value = root.model.data(_prop.currentIndex, ProblemTreeViewItem.Role.Value) + if (new_value != undefined) + { + _prop.currentValue = new_value + } + } + } + + Connections { + target: root + function onFocus_(entityId){ + if(parseInt(_prop.currentId) === parseInt(entityId)){ + _prop.expanded = true + } else { + _prop.expanded = false + } + } + } + + Item { + id: column + + Layout.fillWidth: true + + width: row.implicitWidth + height: Math.max(row.implicitHeight, root.rowHeight) + + RowLayout { + id: row + + anchors.fill: parent + Layout.fillHeight: true + + z: 1 + spacing: root.rowSpacing + + // handle + Loader { + id: indicatorLoader + sourceComponent: handle + + Layout.leftMargin: parent.spacing + + property QtObject currentRow: _prop + + TapHandler { onSingleTapped: _prop.toggle() } + } + + // Content + Loader { + id: contentItemLoader + sourceComponent: contentItem + + Layout.fillWidth: true + height: rowHeight + + property QtObject currentRow: _prop + + Connections { + target: root.model + ignoreUnknownSignals: true + function onDataChanged(modelIndex) { + const changedId = modelIndex.internalId + const currentId = _prop.currentIndex.internalId + if(changedId === currentId){ + contentItemLoader.currentRow.currentData = root.model.data(modelIndex); + } + } + } + } + + TapHandler { + onDoubleTapped: _prop.toggle() + onSingleTapped: { + root.currentItem = _prop.currentItem + root.selectedIndex = _prop.currentIndex + } + } + } + + Loader { + id: hoverLoader + sourceComponent: hoverComponent + + width: row.width + (1 + _prop.depth * rowPadding) + height: parent.height + + x: -(_prop.depth * rowPadding) + z: 0 + + clip: false + + property QtObject currentRow: _prop + + HoverHandler { + onHoveredChanged: { + if(root.hoverEnabled){ + if(hovered && root.hoveredIndex !== _prop.currentIndex) + root.hoveredIndex = _prop.currentIndex + if(!hovered && root.hoveredIndex === _prop.currentIndex) + root.hoveredIndex = null + } + } + } + } + + } + + // loader to populate the children row for each node + Loader { + id: loader + + Layout.fillWidth: true + + source: "ProblemTreeViewItem.qml" + visible: _prop.expanded + + onLoaded: { + item.model = root.model + item.parentIndex = _prop.currentIndex + item.childCount = _prop.itemChildCount + } + + Connections { + target: root.model + ignoreUnknownSignals: true + function onLayoutChanged() { + const parent = root.model.index(index, 0, parentIndex) + loader.item.childCount = root.model.rowCount(parent) + } + + function onToggled() { + root.toggled() + } + } + + Binding { target: loader.item; property: "model"; value: root.model; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "handle"; value: root.handle; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "contentItem"; value: root.contentItem; when: loader.status == Loader.Ready } + + Binding { target: loader.item; property: "currentItem"; value: root.currentItem; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "selectedIndex"; value: root.selectedIndex; when: loader.status == Loader.Ready } + Binding { target: root; property: "currentItem"; value: loader.item.currentItem; when: loader.status == Loader.Ready } + Binding { target: root; property: "selectedIndex"; value: loader.item.selectedIndex; when: loader.status == Loader.Ready } + + Binding { target: loader.item; property: "color"; value: root.color; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "handleColor"; value: root.handleColor; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "hoverEnabled"; value: root.hoverEnabled; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "hoverColor"; value: root.hoverColor; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "selectionEnabled"; value: root.selectionEnabled; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "selectedColor"; value: root.selectedColor; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "selectedItemColor"; value: root.selectedItemColor; when: loader.status == Loader.Ready } + + Binding { target: loader.item; property: "itemLeftPadding"; value: root.rowPadding; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "rowHeight"; value: root.rowHeight; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "rowPadding"; value: root.rowPadding; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "rowSpacing"; value: root.rowSpacing; when: loader.status == Loader.Ready } + Binding { target: loader.item; property: "fontMetrics"; value: root.selectedItemColor; when: loader.status == Loader.Ready } + } + } + } + } + + function focus (entityId) { + root.focus_(entityId) + } +} diff --git a/qml/StatusLayout.qml b/qml/StatusLayout.qml new file mode 100644 index 00000000..4f7a6817 --- /dev/null +++ b/qml/StatusLayout.qml @@ -0,0 +1,305 @@ +// 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.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.15 + +import Theme 1.0 + +Item +{ + id: statusLayout + + // Public properties + enum Status {Closed, Expanded, Collapsed} + property int current_status: StatusLayout.Status.Closed + required property int footer_height + + // Public signals + signal close_status_layout() + signal expand_status_layout() + signal collapse_status_layout() + + // Private properties + property bool filter_visible_: false + + // Private signals + signal focus_entity_(int entityId) + signal clean_filter_() + + // Read only design properties (sizes and colors) + readonly property int tabs_height_: 30 + readonly property int tabs_width_: 100 + readonly property int elements_spacing_: 8 + readonly property int separator_line_: 2 + readonly property string grey_background_: "#eaeaea" + + property int spacingIconLabel: 8 + property int iconSize: 18 + property int firstIndentation: 5 + property int secondIndentation: firstIndentation + iconSize + spacingIconLabel + + + TabView { + id: tab_view + anchors.top: parent.top + anchors.bottom: separator_line.top + width: parent.width + + Tab { + title: "Problems" + Rectangle { + + color: "white" + + ProblemTreeView { + id: status_tree_view + anchors.fill: parent + anchors.margins: 1 + + model: problemModel + + onProblem_focused:{ + collapse_status_layout() + } + + onClean_filter: { + statusLayout.clean_filter_() + } + + Connections { + target: statusLayout + + function onFocus_entity_(entityId) { + status_tree_view.focus_entity(entityId) + } + } + } + } + } + style: TabViewStyle { + frameOverlap: 1 + tab: Rectangle { + color: styleData.selected ? "white" : Theme.lightGrey + implicitWidth: Math.max(text.width + 10, tabs_width_) + implicitHeight: tabs_height_ + radius: 4 + + Rectangle { + width: parent.width + height: parent.height/2 + anchors.bottom: parent.bottom + anchors.left: parent.left + color: parent.color + } + Text { + id: text + anchors.centerIn: parent + text: styleData.title + } + } + tabBar: Rectangle { + anchors.top: parent.top + width: parent.width + height: tabs_height_ + color: grey_background_ + + IconSVG { + id: close_icon + anchors.right: parent.right + anchors.rightMargin: elements_spacing_ *2 + anchors.verticalCenter: parent.verticalCenter + name: "cross" + size: parent.height - elements_spacing_ *3/2 + + MouseArea { + anchors.centerIn: parent + width: parent.width + 2*elements_spacing_ + height: parent.height + 2*elements_spacing_ + + onClicked: { + close_status_layout() + } + } + } + + Rectangle { + id: rect + anchors.right: close_icon.left + anchors.rightMargin: elements_spacing_ *2 + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: elements_spacing_/2 + width: parent.height - elements_spacing_ + height: parent.height - elements_spacing_ + color: "transparent" + + IconSVG { + id: expand_icon + + name: "expand" + size: parent.width + } + + MouseArea { + anchors.centerIn: parent + width: parent.width + 2*elements_spacing_ + height: parent.height + 2*elements_spacing_ + + onClicked: { + if (statusLayout.current_status === StatusLayout.Status.Expanded) + { + collapse_status_layout() + expand_icon.name = "expand" + + } + else if (statusLayout.current_status === StatusLayout.Status.Collapsed) + { + expand_status_layout() + expand_icon.name = "collapse" + } + } + } + } + + IconSVG { + id: filter_empty_icon + visible: !statusLayout.filter_visible_ + anchors.right: rect.left + anchors.rightMargin: elements_spacing_ *2 + anchors.verticalCenter: parent.verticalCenter + name: "filter_empty" + size: parent.height - elements_spacing_ + } + + IconSVG { + id: filter_full_icon + visible: statusLayout.filter_visible_ + anchors.right: rect.left + anchors.rightMargin: elements_spacing_ *2 + anchors.verticalCenter: parent.verticalCenter + name: "filter_full" + size: parent.height - elements_spacing_ + } + Connections { + target: statusLayout + + function onFocus_entity_(entityId) { + statusLayout.filter_visible_ = true + } + + function onClean_filter_() { + statusLayout.filter_visible_ = false + } + } + } + } + } + + Rectangle { + id: separator_line + anchors.bottom: icon_section.top + width: parent.width + height: separator_line_ + + color: Theme.grey + } + + Rectangle { + id: icon_section + anchors.bottom: parent.bottom + height: footer_height + width: parent.width + color: grey_background_ + + Component.onCompleted: { close_status_layout() } + + IconSVG { + id: error_icon + anchors.left: parent.left + anchors.leftMargin: elements_spacing_ + anchors.verticalCenter: parent.verticalCenter + name: "error" + size: parent.height - elements_spacing_ + } + Label { + id: error_value + anchors.left: error_icon.right + anchors.leftMargin: elements_spacing_/2 + anchors.verticalCenter: parent.verticalCenter + text: "0" + } + IconSVG { + id: warning_icon + anchors.left: error_icon.right + anchors.leftMargin: elements_spacing_ * 4 + anchors.verticalCenter: parent.verticalCenter + name: "issues" + size: parent.height - elements_spacing_ + } + Label { + id: warning_value + anchors.left: warning_icon.right + anchors.leftMargin: elements_spacing_/2 + anchors.verticalCenter: parent.verticalCenter + text: "0" + } + + Connections + { + target: controller + function onUpdate_status_counters(errors, warnings) { + error_value.text = errors + warning_value.text = warnings + } + } + /*IconSVG { + id: info_icon + anchors.left: warning_value.right + anchors.leftMargin: elements_spacing_ + name: "info" + size: parent.height - elements_spacing_ -1 + anchors.verticalCenter: parent.verticalCenter + } + Label { + id: info_value + anchors.left: info_icon.right + anchors.leftMargin: elements_spacing_/2 + anchors.verticalCenter: parent.verticalCenter + text: "19" + }*/ + MouseArea { + anchors.fill: parent + onClicked: { + if (current_status === StatusLayout.Status.Collapsed) + { + close_status_layout() + } + else + { + collapse_status_layout() + } + } + } + } + + function filter_problem_log(entityId) { + statusLayout.focus_entity_(entityId) + } +} diff --git a/qml/TabLayout.qml b/qml/TabLayout.qml index 8de5cfaf..baa78f31 100644 --- a/qml/TabLayout.qml +++ b/qml/TabLayout.qml @@ -19,12 +19,18 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Theme 1.0 + Item { id: tabLayout // Public properties property bool fullScreen: false // ChartsLayout inherited var + // Public signals + signal openEntitiesMenu(string domainEntityId, string entityId, string currentAlias, string entityKind) + signal openTopicMenu(string domainEntityId, string domainId, string entityId, string currentAlias, string entityKind) + // Private properties property int current_: 0 // current tab displayed property int last_index_: 1 // force unique idx on QML components @@ -34,6 +40,7 @@ Item { // 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) + signal filter_domain_view_by_topic_(int stack_id, int domain_entity_id, string topic_id) // Read only design properties readonly property int max_tabs_: 15 @@ -95,10 +102,36 @@ Item { width: childrenRect.width spacing: 60 Button { + id: chart_button width: 400; height: 400 + background: Rectangle { + color: Theme.whiteSmoke + border.width: 3 + border.color: chart_button.hovered ? Theme.eProsimaLightBlue : Theme.eProsimaDarkBlue + radius: 40 + + Image { + anchors.centerIn: parent + anchors.verticalCenterOffset: -50 + smooth: true + source: "/resources/images/graphs.svg/" + property int size: 25 + sourceSize.width: size * 16 + sourceSize.height: size * 9 + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom; anchors.bottomMargin: 50 + width: parent.width + text: "Chart View" + horizontalAlignment: Text.AlignHCenter + font.pointSize: 20 + color: chart_button.hovered ? Theme.eProsimaLightBlue : Theme.eProsimaDarkBlue + } + } anchors.verticalCenter: parent.verticalCenter enabled: !disable_chart_selection_ - text: "Chart View" onClicked: { if (!disable_chart_selection_) { @@ -114,9 +147,35 @@ Item { } } Button { + id: domain_view_button width: 400; height: 400 + background: Rectangle { + color: Theme.whiteSmoke + border.width: 3 + border.color: domain_view_button.hovered ? Theme.eProsimaLightBlue : Theme.eProsimaDarkBlue + radius: 40 + + Image { + anchors.centerIn: parent + anchors.verticalCenterOffset: -50 + smooth: true + source: "/resources/images/domain_graph.svg/" + property int size: 30 + sourceSize.width: size * 16 + sourceSize.height: size * 9 + } + + Text { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom; anchors.bottomMargin: 50 + width: parent.width + text: "Domain View" + horizontalAlignment: Text.AlignHCenter + font.pointSize: 20 + color: domain_view_button.hovered ? Theme.eProsimaLightBlue : Theme.eProsimaDarkBlue + } + } anchors.verticalCenter: parent.verticalCenter - text: "Domain View" onClicked: { if (mainApplicationView.monitors == 0) { @@ -166,6 +225,13 @@ Item { } } + onOpenEntitiesMenu: { + tabLayout.openEntitiesMenu(domainEntityId, entityId, currentAlias, entityKind) + } + onOpenTopicMenu: { + tabLayout.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind) + } + Connections { target: tabLayout @@ -177,6 +243,14 @@ Item { domainGraphLayout.load_model() } } + + function onFilter_domain_view_by_topic_(stack_id, domain_entity_id, topic_id) { + if (domainGraphLayout.component_id == stack_id && + domainGraphLayout.entity_id == domain_entity_id) + { + domainGraphLayout.filter_model_by_topic(topic_id) + } + } } } @@ -185,7 +259,7 @@ Item { Connections { target: tabLayout - function onOpen_domain_view_(stack_id, entity_id, domain_id) { + function onOpen_domain_view_(stack_id, entity_id, domain_id, topic_id) { if (stack.stack_id == stack_id) { if (stack.deep > 1) @@ -423,7 +497,8 @@ Item { { for (var i=0; i 0) { controller.domain_click(stack_layout.children[i].currentItem.entity_id) break; @@ -540,4 +615,23 @@ Item { function chartsLayout_saveAllCSV() { chartsLayout.saveAllCSV() } + + function open_topic_view(domainEntityId, domainId, entityId) { + create_new_tab() + open_domain_view_(tabLayout.tab_model_[current_]["stack_id"], domainEntityId, domainId) + filter_domain_view_by_topic_(tabLayout.tab_model_[current_]["stack_id"], domainEntityId, entityId) + } + + function refresh_domain_graph_view(domainEntityId, entityId) { + for (var i=0; i. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import Theme 1.0 + +/* + Menu containing the possible actions that can be performed on any DDS, physical and logical entity. + */ +Menu { + id: topicMenu + property string domainEntityId: "" + property string domainId: "" + property string entityId: "" + property string currentAlias: "" + property string entityKind: "" + + MenuItem { + text: "Change alias" + onTriggered: changeAlias(menu.domainEntityId, menu.entityId, menu.currentAlias, menu.entityKind) + } + MenuItem { + text: "View problems" + onTriggered: filterProblemLog(menu.entityId) + } + MenuItem { + text: "Filter graph view" + onTriggered: openTopicView(menu.domainEntityId, menu.domainId, menu.entityId) + } +} diff --git a/resources/images/domain_graph.svg b/resources/images/domain_graph.svg new file mode 100644 index 00000000..45bc9b26 --- /dev/null +++ b/resources/images/domain_graph.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/collapse/collapse_black.svg b/resources/images/icons/collapse/collapse_black.svg new file mode 100644 index 00000000..9d2bb38b --- /dev/null +++ b/resources/images/icons/collapse/collapse_black.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/collapse/collapse_eProsimaLightBlue.svg b/resources/images/icons/collapse/collapse_eProsimaLightBlue.svg new file mode 100644 index 00000000..32e7586b --- /dev/null +++ b/resources/images/icons/collapse/collapse_eProsimaLightBlue.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/collapse/collapse_grey.svg b/resources/images/icons/collapse/collapse_grey.svg new file mode 100644 index 00000000..1f9eaba8 --- /dev/null +++ b/resources/images/icons/collapse/collapse_grey.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/collapse/collapse_white.svg b/resources/images/icons/collapse/collapse_white.svg new file mode 100644 index 00000000..1be200e0 --- /dev/null +++ b/resources/images/icons/collapse/collapse_white.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/cross/cross_red.svg b/resources/images/icons/cross/cross_red.svg new file mode 100644 index 00000000..ea6821b4 --- /dev/null +++ b/resources/images/icons/cross/cross_red.svg @@ -0,0 +1,5 @@ + + +Layer 1 + + \ No newline at end of file diff --git a/resources/images/icons/error/error_black.svg b/resources/images/icons/error/error_black.svg new file mode 100644 index 00000000..f5c62a0d --- /dev/null +++ b/resources/images/icons/error/error_black.svg @@ -0,0 +1,10 @@ + + +Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/error/error_eProsimaLightBlue.svg b/resources/images/icons/error/error_eProsimaLightBlue.svg new file mode 100644 index 00000000..277fd56f --- /dev/null +++ b/resources/images/icons/error/error_eProsimaLightBlue.svg @@ -0,0 +1,10 @@ + + +Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/error/error_grey.svg b/resources/images/icons/error/error_grey.svg new file mode 100644 index 00000000..43e12705 --- /dev/null +++ b/resources/images/icons/error/error_grey.svg @@ -0,0 +1,10 @@ + + +Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/error/error_red.svg b/resources/images/icons/error/error_red.svg new file mode 100644 index 00000000..33c5370a --- /dev/null +++ b/resources/images/icons/error/error_red.svg @@ -0,0 +1,10 @@ + + +Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/error/error_white.svg b/resources/images/icons/error/error_white.svg new file mode 100644 index 00000000..6ecb6905 --- /dev/null +++ b/resources/images/icons/error/error_white.svg @@ -0,0 +1,10 @@ + + +Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/expand/expand_black.svg b/resources/images/icons/expand/expand_black.svg new file mode 100644 index 00000000..650b4682 --- /dev/null +++ b/resources/images/icons/expand/expand_black.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/expand/expand_eProsimaLightBlue.svg b/resources/images/icons/expand/expand_eProsimaLightBlue.svg new file mode 100644 index 00000000..1d1eab90 --- /dev/null +++ b/resources/images/icons/expand/expand_eProsimaLightBlue.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/expand/expand_grey.svg b/resources/images/icons/expand/expand_grey.svg new file mode 100644 index 00000000..d268e18b --- /dev/null +++ b/resources/images/icons/expand/expand_grey.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/expand/expand_white.svg b/resources/images/icons/expand/expand_white.svg new file mode 100644 index 00000000..09cc7c34 --- /dev/null +++ b/resources/images/icons/expand/expand_white.svg @@ -0,0 +1 @@ +Layer 1 diff --git a/resources/images/icons/filter_empty/filter_empty_black.svg b/resources/images/icons/filter_empty/filter_empty_black.svg new file mode 100644 index 00000000..9a0211e5 --- /dev/null +++ b/resources/images/icons/filter_empty/filter_empty_black.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/filter_empty/filter_empty_eProsimaLightBlue.svg b/resources/images/icons/filter_empty/filter_empty_eProsimaLightBlue.svg new file mode 100644 index 00000000..9003c643 --- /dev/null +++ b/resources/images/icons/filter_empty/filter_empty_eProsimaLightBlue.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/filter_empty/filter_empty_grey.svg b/resources/images/icons/filter_empty/filter_empty_grey.svg new file mode 100644 index 00000000..36c8ba78 --- /dev/null +++ b/resources/images/icons/filter_empty/filter_empty_grey.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/filter_empty/filter_empty_white.svg b/resources/images/icons/filter_empty/filter_empty_white.svg new file mode 100644 index 00000000..f57f44bc --- /dev/null +++ b/resources/images/icons/filter_empty/filter_empty_white.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/filter_full/filter_full_black.svg b/resources/images/icons/filter_full/filter_full_black.svg new file mode 100644 index 00000000..27967f66 --- /dev/null +++ b/resources/images/icons/filter_full/filter_full_black.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/filter_full/filter_full_eProsimaLightBlue.svg b/resources/images/icons/filter_full/filter_full_eProsimaLightBlue.svg new file mode 100644 index 00000000..f5e231a6 --- /dev/null +++ b/resources/images/icons/filter_full/filter_full_eProsimaLightBlue.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/filter_full/filter_full_grey.svg b/resources/images/icons/filter_full/filter_full_grey.svg new file mode 100644 index 00000000..b5652197 --- /dev/null +++ b/resources/images/icons/filter_full/filter_full_grey.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/filter_full/filter_full_white.svg b/resources/images/icons/filter_full/filter_full_white.svg new file mode 100644 index 00000000..21bbf614 --- /dev/null +++ b/resources/images/icons/filter_full/filter_full_white.svg @@ -0,0 +1,3 @@ + + +Layer 1 \ No newline at end of file diff --git a/resources/images/icons/left_arrow/left_arrow_black.svg b/resources/images/icons/left_arrow/left_arrow_black.svg new file mode 100644 index 00000000..c914089c --- /dev/null +++ b/resources/images/icons/left_arrow/left_arrow_black.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/left_arrow/left_arrow_eProsimaLightBlue.svg b/resources/images/icons/left_arrow/left_arrow_eProsimaLightBlue.svg new file mode 100644 index 00000000..624805e0 --- /dev/null +++ b/resources/images/icons/left_arrow/left_arrow_eProsimaLightBlue.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/left_arrow/left_arrow_grey.svg b/resources/images/icons/left_arrow/left_arrow_grey.svg new file mode 100644 index 00000000..f2b5779e --- /dev/null +++ b/resources/images/icons/left_arrow/left_arrow_grey.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/left_arrow/left_arrow_white.svg b/resources/images/icons/left_arrow/left_arrow_white.svg new file mode 100644 index 00000000..6b9c5105 --- /dev/null +++ b/resources/images/icons/left_arrow/left_arrow_white.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/right_arrow/right_arrow_black.svg b/resources/images/icons/right_arrow/right_arrow_black.svg new file mode 100644 index 00000000..00768271 --- /dev/null +++ b/resources/images/icons/right_arrow/right_arrow_black.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/right_arrow/right_arrow_eProsimaLightBlue.svg b/resources/images/icons/right_arrow/right_arrow_eProsimaLightBlue.svg new file mode 100644 index 00000000..ee6300c1 --- /dev/null +++ b/resources/images/icons/right_arrow/right_arrow_eProsimaLightBlue.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/right_arrow/right_arrow_grey.svg b/resources/images/icons/right_arrow/right_arrow_grey.svg new file mode 100644 index 00000000..135d1b59 --- /dev/null +++ b/resources/images/icons/right_arrow/right_arrow_grey.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/right_arrow/right_arrow_white.svg b/resources/images/icons/right_arrow/right_arrow_white.svg new file mode 100644 index 00000000..7bf3f71a --- /dev/null +++ b/resources/images/icons/right_arrow/right_arrow_white.svg @@ -0,0 +1,7 @@ + + + + Layer 1 + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_left_arrow/rounded_left_arrow_black.svg b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_black.svg new file mode 100644 index 00000000..69b4af63 --- /dev/null +++ b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_black.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_left_arrow/rounded_left_arrow_eProsimaLightBlue.svg b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_eProsimaLightBlue.svg new file mode 100644 index 00000000..e77aebf4 --- /dev/null +++ b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_eProsimaLightBlue.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_left_arrow/rounded_left_arrow_grey.svg b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_grey.svg new file mode 100644 index 00000000..8e1c8a0c --- /dev/null +++ b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_grey.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_left_arrow/rounded_left_arrow_white.svg b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_white.svg new file mode 100644 index 00000000..031e91bb --- /dev/null +++ b/resources/images/icons/rounded_left_arrow/rounded_left_arrow_white.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_right_arrow/rounded_right_arrow_black.svg b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_black.svg new file mode 100644 index 00000000..ace9468d --- /dev/null +++ b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_black.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_right_arrow/rounded_right_arrow_eProsimaLightBlue.svg b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_eProsimaLightBlue.svg new file mode 100644 index 00000000..613e3233 --- /dev/null +++ b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_eProsimaLightBlue.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_right_arrow/rounded_right_arrow_grey.svg b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_grey.svg new file mode 100644 index 00000000..3ef61017 --- /dev/null +++ b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_grey.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/rounded_right_arrow/rounded_right_arrow_white.svg b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_white.svg new file mode 100644 index 00000000..33cb74e1 --- /dev/null +++ b/resources/images/icons/rounded_right_arrow/rounded_right_arrow_white.svg @@ -0,0 +1,12 @@ + + triangle-filled + + + Layer 1 + + + + + + + \ No newline at end of file diff --git a/resources/images/topic_graph.svg b/resources/images/topic_graph.svg new file mode 100644 index 00000000..a1c47775 --- /dev/null +++ b/resources/images/topic_graph.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Layer 1 \ No newline at end of file diff --git a/src/Engine.cpp b/src/Engine.cpp index 873d9461..42f74da3 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -87,6 +87,10 @@ QObject* Engine::enable() generate_new_status_info_(); fill_status_(); + // Creates a default json structure for problems and fills the tree model with it + problem_model_ = new models::ProblemTreeModel(); + update_problem(backend::ID_ALL, backend::StatusKind::INVALID); + source_entity_id_model_ = new models::ListModel(new models::EntityItem()); fill_available_entity_id_list_(backend::EntityKind::HOST, "getDataDialogSourceEntityId"); destination_entity_id_model_ = new models::ListModel(new models::EntityItem()); @@ -109,6 +113,7 @@ QObject* Engine::enable() rootContext()->setContextProperty("issueModel", issue_model_); rootContext()->setContextProperty("logModel", log_model_); rootContext()->setContextProperty("statusModel", status_model_); + rootContext()->setContextProperty("problemModel", problem_model_); rootContext()->setContextProperty("entityModelFirst", source_entity_id_model_); rootContext()->setContextProperty("entityModelSecond", destination_entity_id_model_); @@ -118,6 +123,7 @@ QObject* Engine::enable() rootContext()->setContextProperty("controller", controller_); addImportPath(":/imports"); + addImportPath(":/imports/TreeView"); load(QUrl(QLatin1String("qrc:/qml/main.qml"))); // Connect Callback Listener to this object @@ -127,6 +133,12 @@ QObject* Engine::enable() this, &Engine::new_callback_slot); + QObject::connect( + this, + &Engine::new_problem_callback_signal, + this, + &Engine::new_problem_callback_slot); + // Set enable as True enabled_ = true; @@ -186,6 +198,11 @@ Engine::~Engine() delete status_model_; } + if (problem_model_) + { + //delete problem_model_; + } + // Auxiliar models if (source_entity_id_model_) { @@ -833,12 +850,27 @@ void Engine::process_callback_queue() } } +void Engine::process_problem_callback_queue() +{ + // It iterates while run_ is activate and the queue has elements + while (!problem_callback_queue_.empty()) + { + process_problem_callback_(); + } +} + bool Engine::are_callbacks_to_process_() { std::lock_guard ml(callback_queue_mutex_); return callback_queue_.empty(); } +bool Engine::are_problem_callbacks_to_process_() +{ + std::lock_guard ml(problem_callback_queue_mutex_); + return problem_callback_queue_.empty(); +} + bool Engine::add_callback( backend::Callback callback) { @@ -851,11 +883,28 @@ bool Engine::add_callback( return true; } +bool Engine::add_callback( + backend::ProblemCallback problem_callback) +{ + std::lock_guard ml(problem_callback_queue_mutex_); + problem_callback_queue_.append(problem_callback); + + // Emit signal to specify there are new data + emit new_problem_callback_signal(); + + return true; +} + void Engine::new_callback_slot() { process_callback_queue(); } +void Engine::new_problem_callback_slot() +{ + process_problem_callback_queue(); +} + bool Engine::process_callback_() { backend::Callback first_callback; @@ -871,6 +920,21 @@ bool Engine::process_callback_() return read_callback_(first_callback); } +bool Engine::process_problem_callback_() +{ + backend::ProblemCallback first_problem_callback; + + { + std::lock_guard ml(problem_callback_queue_mutex_); + first_problem_callback = problem_callback_queue_.front(); + problem_callback_queue_.pop_front(); + } + + qDebug() << "Processing problem callback: " << backend::backend_id_to_models_id(first_problem_callback.entity_id); + + return read_callback_(first_problem_callback); +} + bool Engine::read_callback_( backend::Callback callback) { @@ -897,6 +961,283 @@ bool Engine::read_callback_( callback.entity_id, callback.entity_kind, callback.is_update); } +bool Engine::read_callback_( + backend::ProblemCallback problem_callback) +{ + // It should not read callbacks while a domain is being initialized + std::lock_guard lock(initializing_monitor_); + + // update model + return update_problem(problem_callback.entity_id, problem_callback.status_kind); +} + +bool Engine::update_problem( + const backend::EntityId& id, + backend::StatusKind kind) +{ + int counter = 0; + auto empty_item = new models::ProblemTreeItem(backend::ID_ALL, std::string("No issues found"), false, std::string("")); + if (id == backend::ID_ALL) + { + problem_model_->addTopLevelItem(empty_item); + } + else + { + backend::EntityStatus new_status = backend::EntityStatus::OK; + std::string description = backend::problem_description(kind); + + switch (kind) + { + case backend::StatusKind::DEADLINE_MISSED: + { + backend::DeadlineMissedSample sample; + backend_connection_.get_status_data(id, sample); + if (sample.status != backend::EntityStatus::OK) + { + auto entity_item = problem_model_->getTopLevelItem( + id, backend_connection_.get_name(id), + sample.status == backend::EntityStatus::ERROR, description); + new_status = sample.status; + std::string handle_string; + auto deadline_missed_item = new models::ProblemTreeItem(id, kind, std::string("Deadline missed"), + sample.status == backend::EntityStatus::ERROR, std::string(""), description); + auto total_count_item = new models::ProblemTreeItem(id, kind, std::string("Total count:"), + sample.status == backend::EntityStatus::ERROR, + std::to_string(sample.deadline_missed_status.total_count()), description); + for (uint8_t handler : sample.deadline_missed_status.last_instance_handle()) + { + handle_string = handle_string + std::to_string(handler); + } + auto last_instance_handle_item = new models::ProblemTreeItem(id, kind, + std::string("Last instance handle:"), + sample.status == backend::EntityStatus::ERROR, handle_string, description); + problem_model_->addItem(deadline_missed_item, total_count_item); + problem_model_->addItem(deadline_missed_item, last_instance_handle_item); + problem_model_->addItem(entity_item, deadline_missed_item); + counter = entity_item->recalculate_entity_counter(); + } + break; + } + case backend::StatusKind::INCOMPATIBLE_QOS: + { + backend::IncompatibleQosSample sample; + backend_connection_.get_status_data(id, sample); + if (sample.status != backend::EntityStatus::OK) + { + std::string fastdds_version = "v2.12.0"; + auto entity_item = problem_model_->getTopLevelItem( + id, backend_connection_.get_name(id), + sample.status == backend::EntityStatus::ERROR, description); + new_status = sample.status; + auto incompatible_qos_item = new models::ProblemTreeItem(id, kind, std::string("Incompatible QoS"), + sample.status == backend::EntityStatus::ERROR, std::string(""), description); + for (eprosima::fastdds::statistics::QosPolicyCount_s policy : sample.incompatible_qos_status.policies()) + { + if (policy.count() > 0) + { + auto policy_item = new models::ProblemTreeItem(id, kind, + std::string(backend::policy_id_to_string(policy.policy_id()) + ":"), + sample.status == backend::EntityStatus::ERROR, + std::to_string(policy.count()), + std::string("Check for compatible rules ") + + std::string("here")); + problem_model_->addItem(incompatible_qos_item, policy_item); + } + } + problem_model_->addItem(entity_item, incompatible_qos_item); + counter = entity_item->recalculate_entity_counter(); + } + break; + } + case backend::StatusKind::INCONSISTENT_TOPIC: + { + backend::InconsistentTopicSample sample; + backend_connection_.get_status_data(id, sample); + if (sample.status != backend::EntityStatus::OK) + { + auto entity_item = problem_model_->getTopLevelItem( + id, backend_connection_.get_name(id), + sample.status == backend::EntityStatus::ERROR, description); + new_status = sample.status; + auto inconsistent_topic_item = new models::ProblemTreeItem(id, kind, std::string("Inconsistent topics:"), + sample.status == backend::EntityStatus::ERROR, + std::to_string(sample.inconsistent_topic_status.total_count()), description); + problem_model_->addItem(entity_item, inconsistent_topic_item); + counter = entity_item->recalculate_entity_counter(); + } + break; + } + case backend::StatusKind::LIVELINESS_LOST: + { + backend::LivelinessLostSample sample; + backend_connection_.get_status_data(id, sample); + if (sample.status != backend::EntityStatus::OK) + { + auto entity_item = problem_model_->getTopLevelItem( + id, backend_connection_.get_name(id), + sample.status == backend::EntityStatus::ERROR, description); + new_status = sample.status; + auto liveliness_lost_item = new models::ProblemTreeItem(id, kind, std::string("Liveliness lost:"), + sample.status == backend::EntityStatus::ERROR, + std::to_string(sample.liveliness_lost_status.total_count()), description); + problem_model_->addItem(entity_item, liveliness_lost_item); + counter = entity_item->recalculate_entity_counter(); + } + break; + } + case backend::StatusKind::SAMPLE_LOST: + { + backend::SampleLostSample sample; + backend_connection_.get_status_data(id, sample); + if (sample.status != backend::EntityStatus::OK) + { + auto entity_item = problem_model_->getTopLevelItem( + id, backend_connection_.get_name(id), + sample.status == backend::EntityStatus::ERROR, description); + new_status = sample.status; + auto samples_lost_item = new models::ProblemTreeItem(id, kind, std::string("Samples lost:"), + sample.status == backend::EntityStatus::ERROR, + std::to_string(sample.sample_lost_status.total_count()), description); + problem_model_->addItem(entity_item, samples_lost_item); + counter = entity_item->recalculate_entity_counter(); + } + break; + } + + case backend::StatusKind::CONNECTION_LIST: + case backend::StatusKind::LIVELINESS_CHANGED: + case backend::StatusKind::PROXY: + //case backend::StatusKind::STATUSES_SIZE: + default: + { + // No problem status updates, as always returns OK + break; + } + } + if (new_status != backend::EntityStatus::OK) + { + std::map::iterator it; + uint32_t total_counter = 0; + bool found = false; + if (new_status == backend::EntityStatus::ERROR) + { + for (it = controller_->status_counters.errors.begin(); it != controller_->status_counters.errors.end(); it++) + { + if (it->first == id) + { + //element found; + found = true; + it->second = counter; + } + total_counter += it->second; + } + if (!found) + { + controller_->status_counters.errors.insert(std::pair(id, counter)); + } + controller_->status_counters.total_errors = total_counter; + } + else if (new_status == backend::EntityStatus::WARNING) + { + for (it = controller_->status_counters.warnings.begin(); it != controller_->status_counters.warnings.end(); it++) + { + if (it->first == id) + { + //element found; + found = true; + it->second = counter; + } + total_counter += it->second; + } + if (!found) + { + controller_->status_counters.warnings.insert(std::pair(id, counter)); + } + controller_->status_counters.total_warnings = total_counter; + } + // notify problem model layout changed to refresh layout view + emit problem_model_->layoutAboutToBeChanged(); + + emit controller_->update_status_counters( + QString::number(controller_->status_counters.total_errors), + QString::number(controller_->status_counters.total_warnings)); + + // remove empty message if exists + if (problem_model_->is_empty()) + { + problem_model_->removeEmptyItem(); + } + // notify problem model layout changed to refresh layout view + emit problem_model_->layoutChanged(); + } + } + return true; +} + +bool Engine::update_problem_entities( + const backend::EntityId& id) +{ + // check if there are entities in the problem model + if (!problem_model_->is_empty()) + { + // get info from id + EntityInfo entity_info = backend_connection_.get_info(id); + + // update problem model if not alive + if (!entity_info["alive"]) + { + // remove item from tree + problem_model_->removeItem(problem_model_->getTopLevelItem(id, "", false, "")); + + // add empty item if removed last item + if (problem_model_->rowCount(problem_model_->rootIndex()) == 0) + { + problem_model_->addTopLevelItem(new models::ProblemTreeItem(backend::ID_ALL, std::string("No issues found"), false, std::string(""))); + } + + + uint32_t error_checker = 0 - 100; + // update error counter + std::map::iterator err_it = controller_->status_counters.errors.find(id); + if(err_it != controller_->status_counters.errors.end()) + { + //element found; + controller_->status_counters.total_errors -= err_it->second; + if (controller_->status_counters.total_errors > error_checker) + { + controller_->status_counters.total_errors = 0; + } + } + controller_->status_counters.errors.erase(id); + + // update warning counter + std::map::iterator warn_it = controller_->status_counters.warnings.find(id); + if(warn_it != controller_->status_counters.warnings.end()) + { + //element found; + controller_->status_counters.total_warnings -= warn_it->second; + if (controller_->status_counters.total_warnings > error_checker) + { + controller_->status_counters.total_warnings = 0; + } + } + controller_->status_counters.warnings.erase(id); + + // refresh layout + emit problem_model_->layoutAboutToBeChanged(); + + emit controller_->update_status_counters( + QString::number(controller_->status_counters.total_errors), + QString::number(controller_->status_counters.total_warnings)); + } + return true; + } + return false; +} + bool Engine::update_entity_generic( backend::EntityId entity_id, backend::EntityKind entity_kind, @@ -926,14 +1267,17 @@ bool Engine::update_entity_generic( entity_id, &Engine::update_topic, !is_update, is_last_clicked); case backend::EntityKind::PARTICIPANT: + update_problem_entities(entity_id); return update_entity( entity_id, &Engine::update_participant, !is_update, is_last_clicked); case backend::EntityKind::DATAWRITER: + update_problem_entities(entity_id); return update_entity( entity_id, &Engine::update_datawriter, !is_update, is_last_clicked); case backend::EntityKind::DATAREADER: + update_problem_entities(entity_id); return update_entity( entity_id, &Engine::update_datareader, !is_update, is_last_clicked); diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index a00375ed..e224b3ae 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -169,4 +169,14 @@ void Listener::on_topic_discovery( } } +void Listener::on_problem_reported( + EntityId domain_id, + EntityId entity_id, + StatusKind status_kind) +{ + //qDebug() << "PROBLEM REPORTED OF " << backend::backend_id_to_models_id(entity_id) + // << " IN DOMAIN " << backend::backend_id_to_models_id(domain_id); + engine_->add_callback(ProblemCallback(domain_id, entity_id, status_kind)); +} + } //namespace backend diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 87e2f2eb..4353e5f8 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -718,6 +718,63 @@ bool SyncBackendConnection::data_available( return !data.empty(); } +void SyncBackendConnection::get_status_data( + EntityId id, + ConnectionListSample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + +void SyncBackendConnection::get_status_data( + EntityId id, + DeadlineMissedSample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + +void SyncBackendConnection::get_status_data( + EntityId id, + IncompatibleQosSample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + +void SyncBackendConnection::get_status_data( + EntityId id, + InconsistentTopicSample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + +void SyncBackendConnection::get_status_data( + EntityId id, + LivelinessChangedSample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + +void SyncBackendConnection::get_status_data( + EntityId id, + LivelinessLostSample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + +void SyncBackendConnection::get_status_data( + EntityId id, + ProxySample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + +void SyncBackendConnection::get_status_data( + EntityId id, + SampleLostSample& sample) +{ + StatisticsBackend::get_status_data(id, sample); +} + + bool SyncBackendConnection::build_source_target_entities_vectors( DataKind data_kind, EntityId source_entity_id, diff --git a/src/backend/backend_utils.cpp b/src/backend/backend_utils.cpp index 034a66e1..063e02da 100644 --- a/src/backend/backend_utils.cpp +++ b/src/backend/backend_utils.cpp @@ -123,6 +123,35 @@ std::string statistic_kind_to_string( } } +std::string status_kind_to_string( + const StatusKind& status_kind) +{ + switch (status_kind) + { + case StatusKind::CONNECTION_LIST: + return "CONNECTION_LIST"; + case StatusKind::DEADLINE_MISSED: + return "DEADLINE_MISSED"; + case StatusKind::INCOMPATIBLE_QOS: + return "INCOMPATIBLE_QOS"; + case StatusKind::INCONSISTENT_TOPIC: + return "INCONSISTENT_TOPIC"; + case StatusKind::LIVELINESS_CHANGED: + return "LIVELINESS_CHANGED"; + case StatusKind::LIVELINESS_LOST: + return "LIVELINESS_LOST"; + case StatusKind::PROXY: + return "PROXY"; + case StatusKind::SAMPLE_LOST: + return "SAMPLE_LOST"; + case StatusKind::STATUSES_SIZE: + return "STATUSES_SIZE"; + case StatusKind::INVALID: + default: + return "INVALID"; + } +} + std::string data_kind_to_string( const DataKind& data_kind) { @@ -378,4 +407,144 @@ std::string timestamp_to_string( return ss.str(); } +std::string policy_id_to_string( + const uint32_t& id) +{ + switch (id) + { + case 0: // INVALID_QOS_POLICY_ID + default: + return "INVALID"; + case 1: // USERDATA_QOS_POLICY_ID + return "UserData"; + case 2: // DURABILITY_QOS_POLICY_ID + return "Durability"; + case 3: // PRESENTATION_QOS_POLICY_ID + return "Presentation"; + case 4: // DEADLINE_QOS_POLICY_ID + return "Deadline"; + case 5: // LATENCYBUDGET_QOS_POLICY_ID + return "Latency budget"; + case 6: // OWNERSHIP_QOS_POLICY_ID + return "Ownership"; + case 7: // OWNERSHIPSTRENGTH_QOS_POLICY_ID + return "Ownership strength"; + case 8: // LIVELINESS_QOS_POLICY_ID + return "Liveliness"; + case 9: // TIMEBASEDFILTER_QOS_POLICY_ID + return "Time-based filter"; + case 10: // PARTITION_QOS_POLICY_ID + return "Partition"; + case 11: // RELIABILITY_QOS_POLICY_ID + return "Reliability"; + case 12: // DESTINATIONORDER_QOS_POLICY_ID + return "Destination order"; + case 13: // HISTORY_QOS_POLICY_ID + return "History"; + case 14: // RESOURCELIMITS_QOS_POLICY_ID + return "Resource limits"; + case 15: // ENTITYFACTORY_QOS_POLICY_ID + return "Entity factory"; + case 16: // WRITERDATALIFECYCLE_QOS_POLICY_ID + return "Writer data lifecycle"; + case 17: // READERDATALIFECYCLE_QOS_POLICY_ID + return "Reader data lifecycle"; + case 18: // TOPICDATA_QOS_POLICY_ID + return "Topic data"; + case 19: // GROUPDATA_QOS_POLICY_ID + return "Group data"; + case 20: // TRANSPORTPRIORITY_QOS_POLICY_ID + return "Transport priority"; + case 21: // LIFESPAN_QOS_POLICY_ID + return "Lifespan"; + case 22: // DURABILITYSERVICE_QOS_POLICY_ID + return "Durability service"; + case 23: // DATAREPRESENTATION_QOS_POLICY_ID + return "Data representation"; + case 24: // TYPECONSISTENCYENFORCEMENT_QOS_POLICY_ID + return "Type consistency enforcement"; + case 25: // DISABLEPOSITIVEACKS_QOS_POLICY_ID + return "Disable positive acks"; + case 26: // PARTICIPANTRESOURCELIMITS_QOS_POLICY_ID + return "Participant resource limits"; + case 27: // PROPERTYPOLICY_QOS_POLICY_ID + return "Property policy"; + case 28: // PUBLISHMODE_QOS_POLICY_ID + return "Publish mode"; + case 29: // READERRESOURCELIMITS_QOS_POLICY_ID + return "Reader resource limits"; + case 30: // RTPSENDPOINT_QOS_POLICY_ID + return "RTPS endpoint"; + case 31: // RTPSRELIABLEREADER_QOS_POLICY_ID + return "RTPS reliable reader"; + case 32: // RTPSRELIABLEWRITER_QOS_POLICY_ID + return "RTPS reliable writer"; + case 33: // TRANSPORTCONFIG_QOS_POLICY_ID + return "Transport config"; + case 34: // TYPECONSISTENCY_QOS_POLICY_ID + return "Type consistency"; + case 35: // WIREPROTOCOLCONFIG_QOS_POLICY_ID + return "Wire protocol config"; + case 36: // WRITERRESOURCELIMITS_QOS_POLICY_ID + return "Writer resource limits"; + } +} + +std::string problem_description( + const backend::StatusKind kind) +{ + switch (kind) { + case backend::StatusKind::CONNECTION_LIST: + return "List of connections that the entity is using"; + case backend::StatusKind::DEADLINE_MISSED: + return "Number of deadlines missed that were registered in the entity"; + case backend::StatusKind::INCOMPATIBLE_QOS: + return "Tracks the number of incompatible QoS detected in the entity"; + case backend::StatusKind::INCONSISTENT_TOPIC: + return "Status of Inconsistent topics of the topic of the entity"; + case backend::StatusKind::LIVELINESS_CHANGED: + return "Tracks the number of times that liveliness status changed (reader side)"; + case backend::StatusKind::LIVELINESS_LOST: + return "Tracks the number of times that liveliness was lost (writer side)"; + case backend::StatusKind::PROXY: + return "Collection of Parameters describing the Proxy Data of the entity"; + case backend::StatusKind::SAMPLE_LOST: + return "Tracks the number of times that the entity lost samples"; + //case backend::StatusKind::STATUSES_SIZE: + // return ""; + default: + case backend::StatusKind::INVALID: + return ""; + } +} + +std::string policy_documentation_description( + const uint32_t& id) +{ + switch (id) + { + case 2: // DURABILITY_QOS_POLICY_ID + return "#durability-compatibilityrule"; + case 3: // PRESENTATION_QOS_POLICY_ID + return "#presentation-compatibilityrule"; + case 4: // DEADLINE_QOS_POLICY_ID + return "#compatibility-rule"; + case 5: // LATENCYBUDGET_QOS_POLICY_ID + return "#latencybudget-compatibilityrule"; + case 6: // OWNERSHIP_QOS_POLICY_ID + return "#ownership-compatibilityrule"; + case 8: // LIVELINESS_QOS_POLICY_ID + return "#liveliness-compatibilityrule"; + case 11: // RELIABILITY_QOS_POLICY_ID + return "#reliability-compatibilityrule"; + case 12: // DESTINATIONORDER_QOS_POLICY_ID + return "#destinationorder-compatibilityrule"; + case 14: // RESOURCELIMITS_QOS_POLICY_ID + return "#consistency-rule"; + default: + return ""; + } +} + + } // namespace backend diff --git a/src/model/tree/ProblemTreeItem.cpp b/src/model/tree/ProblemTreeItem.cpp new file mode 100644 index 00000000..af09e651 --- /dev/null +++ b/src/model/tree/ProblemTreeItem.cpp @@ -0,0 +1,318 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Maurizio Ingrassia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// 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 . + +#include + +#include +#include +#include +#include + +namespace models { + +ProblemTreeItem::ProblemTreeItem() + : parent_item_(nullptr) + , id_(backend::ID_ALL) + , kind_(backend::StatusKind::INVALID) + , name_() + , is_status_error_(false) + , value_() + , description_() + , is_active_(true) + , id_variant_(QVariant(backend::backend_id_to_models_id(id_))) + , kind_variant_(QVariant(QString::fromStdString(backend::status_kind_to_string(kind_)))) + , name_variant_(QVariant()) + , is_status_error_variant_(QVariant(false)) + , value_variant_(QVariant()) + , description_variant_(QVariant()) + , is_active_variant_(QVariant(true)) +{ +} + +ProblemTreeItem::ProblemTreeItem( + const QVariant& data) + : parent_item_(nullptr) + , id_(backend::ID_ALL) + , kind_(backend::StatusKind::INVALID) + , name_(data.toString().toStdString()) + , is_status_error_(false) + , value_() + , description_() + , is_active_(true) + , id_variant_(QVariant(backend::backend_id_to_models_id(id_))) + , kind_variant_(QVariant(QString::fromStdString(backend::status_kind_to_string(kind_)))) + , name_variant_(QVariant(QString::fromStdString(name_))) + , is_status_error_variant_(QVariant(false)) + , value_variant_(QVariant()) + , description_variant_(QVariant()) + , is_active_variant_(QVariant(true)) +{ +} + +ProblemTreeItem::ProblemTreeItem( + const backend::EntityId& id, + const std::string& name, + const bool& is_error, + const std::string& description) + : parent_item_(nullptr) + , id_(id) + , kind_(backend::StatusKind::INVALID) + , name_(name) + , is_status_error_(is_error) + , value_() + , description_(description) + , is_active_(true) + , id_variant_(QVariant(backend::backend_id_to_models_id(id_))) + , kind_variant_(QVariant(QString::fromStdString(backend::status_kind_to_string(kind_)))) + , name_variant_(QVariant(QString::fromStdString(name))) + , is_status_error_variant_(QVariant(is_error)) + , value_variant_(QVariant()) + , description_variant_(QVariant(QString::fromStdString(description))) + , is_active_variant_(QVariant(true)) +{ +} + +ProblemTreeItem::ProblemTreeItem( + const backend::EntityId& id, + const backend::StatusKind& kind, + const std::string& name, + const bool& is_error, + const std::string& value, + const std::string& description) + : parent_item_(nullptr) + , id_(id) + , kind_(kind) + , name_(name) + , is_status_error_(is_error) + , value_(value) + , description_(description) + , is_active_(true) + , id_variant_(QVariant(backend::backend_id_to_models_id(id_))) + , kind_variant_(QVariant(QString::fromStdString(backend::status_kind_to_string(kind_)))) + , name_variant_(QVariant(QString::fromStdString(name_))) + , is_status_error_variant_(QVariant(is_error)) + , value_variant_(QVariant(QString::fromStdString(value))) + , description_variant_(QVariant(QString::fromStdString(description))) + , is_active_variant_(QVariant(true)) +{ +} + +ProblemTreeItem::~ProblemTreeItem() +{ + qDeleteAll(child_items_); +} + +ProblemTreeItem* ProblemTreeItem::parentItem() +{ + return parent_item_; +} + +void ProblemTreeItem::setParentItem( + ProblemTreeItem* parentItem) +{ + parent_item_ = parentItem; +} + +void ProblemTreeItem::appendChild( + ProblemTreeItem* item) +{ + if (item && !child_items_.contains(item)) + { + child_items_.append(item); + } +} + +void ProblemTreeItem::removeChild( + ProblemTreeItem* item) +{ + if (item) + { + child_items_.removeAll(item); + } +} + +ProblemTreeItem* ProblemTreeItem::child( + int row) +{ + return child_items_.value(row); +} + +int ProblemTreeItem::childCount() const +{ + return child_items_.count(); +} + +const QVariant& ProblemTreeItem::data() const +{ + return this->data(models::ProblemTreeModel::ModelItemRoles::nameRole); +} + +const QVariant& ProblemTreeItem::data( + int role) const +{ + switch (role) + { + case models::ProblemTreeModel::ModelItemRoles::idRole: + return this->entity_id(); + case models::ProblemTreeModel::ModelItemRoles::statusRole: + return this->status(); + case models::ProblemTreeModel::ModelItemRoles::kindRole: + return this->status_kind(); + case models::ProblemTreeModel::ModelItemRoles::valueRole: + return this->value(); + case models::ProblemTreeModel::ModelItemRoles::descriptionRole: + return this->description(); + case models::ProblemTreeModel::ModelItemRoles::aliveRole: + return this->alive(); + case models::ProblemTreeModel::ModelItemRoles::nameRole: + default: + return this->name(); + } +} + +const QVariant& ProblemTreeItem::entity_id() const +{ + return id_variant_; +} + +const QVariant& ProblemTreeItem::status_kind() const +{ + return kind_variant_; +} + +const QVariant& ProblemTreeItem::name() const +{ + return name_variant_; +} + +const QVariant& ProblemTreeItem::status() const +{ + return is_status_error_variant_; +} + +const QVariant& ProblemTreeItem::value() const +{ + return value_variant_; +} + +const QVariant& ProblemTreeItem::description() const +{ + return description_variant_; +} + +const QVariant& ProblemTreeItem::alive() const +{ + return is_active_variant_; +} + +void ProblemTreeItem::setData( + const QVariant& data) +{ + name_variant_ = data; +} + +bool ProblemTreeItem::isLeaf() const +{ + return child_items_.isEmpty(); +} + +int ProblemTreeItem::depth() const +{ + int depth = 0; + ProblemTreeItem* anchestor = parent_item_; + while (anchestor) + { + ++depth; + anchestor = anchestor->parentItem(); + } + + return depth; +} + +int ProblemTreeItem::row() const +{ + if (parent_item_) + { + return parent_item_->child_items_.indexOf(const_cast(this)); + } + + return 0; +} + +backend::EntityId ProblemTreeItem::id() +{ + return id_; +} + +backend::StatusKind ProblemTreeItem::kind() +{ + return kind_; +} + + +int ProblemTreeItem::recalculate_entity_counter() +{ + int count = 0; + // check if top level item / entity item + if (id_ != backend::ID_ALL && kind_ == backend::StatusKind::INVALID) + { + for (int i = 0; i < child_items_.count(); i++) + { + try + { + if (child_items_.value(i)->childCount() > 0) + { + for (int j = 0; j < child_items_.value(i)->childCount(); j++) + { + count += child_items_.value(i)->child(j)->value().toInt(); + } + } + count += child_items_.value(i)->value().toInt(); + } + catch (...) {} + } + value_ = std::to_string(count); + value_variant_ = QVariant(QString::fromStdString(value_)); + } + return count; +} + +} // namespace models diff --git a/src/model/tree/ProblemTreeModel.cpp b/src/model/tree/ProblemTreeModel.cpp new file mode 100644 index 00000000..2576775f --- /dev/null +++ b/src/model/tree/ProblemTreeModel.cpp @@ -0,0 +1,375 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021 Maurizio Ingrassia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// 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 . + +#include + +#include +#include + +#include + +namespace models { + +ProblemTreeModel::ProblemTreeModel( + QObject* parent) + : QAbstractItemModel(parent) + , root_item_{ new ProblemTreeItem() } + , is_empty_(false) +{ + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); +} + +ProblemTreeModel::~ProblemTreeModel() +{ + delete root_item_; +} + +int ProblemTreeModel::rowCount( + const QModelIndex& parent) const +{ + if (!parent.isValid()) + { + return root_item_->childCount(); + } + + return internalPointer(parent)->childCount(); +} + +int ProblemTreeModel::columnCount( + const QModelIndex& /*parent*/) const +{ + // This is basically flatten as a list model + return 1; +} + +QModelIndex ProblemTreeModel::index( + const int row, + const int column, + const QModelIndex& parent) const +{ + if (!hasIndex(row, column, parent)) + { + return {}; + } + + ProblemTreeItem* item = root_item_; + if (parent.isValid()) + { + item = internalPointer(parent); + } + + if (auto child = item->child(row)) + { + return createIndex(row, column, child); + } + + return {}; +} + +QModelIndex ProblemTreeModel::parent( + const QModelIndex& index) const +{ + if (!index.isValid()) + { + return {}; + } + + ProblemTreeItem* childItem = internalPointer(index); + ProblemTreeItem* parentItem = childItem->parentItem(); + + if (!parentItem) + { + return {}; + } + + if (parentItem == root_item_) + { + return {}; + } + + return createIndex(parentItem->row(), 0, parentItem); +} + +QVariant ProblemTreeModel::data( + const QModelIndex& index, + const int role) const +{ + if (!index.isValid()/* || role != Qt::DisplayRole*/) + { + return QVariant(); + } + + return internalPointer(index)->data(role); +} + +bool ProblemTreeModel::setData( + const QModelIndex& index, + const QVariant& value, + int /*role*/) +{ + if (!index.isValid()) + { + return false; + } + + if (auto item = internalPointer(index)) + { + item->setData(value); + emit dataChanged(index, index, {Qt::EditRole}); + } + + return false; +} + +void ProblemTreeModel::addTopLevelItem( + ProblemTreeItem* child) +{ + if (child) + { + addItem(root_item_, child); + if (child->id() == backend::ID_ALL &&root_item_->childCount() == 1) + { + is_empty_ = true; + } + } +} + +void ProblemTreeModel::addItem( + ProblemTreeItem* parent, + ProblemTreeItem* child) +{ + if (!child || !parent) + { + return; + } + + + // if parent is topLevelItem (entity item) + if (parent->id() != backend::ID_ALL && parent->kind() == backend::StatusKind::INVALID) + { + // For each problem in the entity item + for (int i=0; ichildCount(); i++) + { + // if overriding problem, remove previous problem + if (parent->child(i)->id() == child->id() && parent->child(i)->kind() == child->kind()) + { + emit layoutAboutToBeChanged(); + beginRemoveRows(QModelIndex(), qMax(parent->childCount() - 1, 0), qMax(parent->childCount(), 0)); + parent->removeChild(parent->child(i)); + endRemoveRows(); + emit layoutChanged(); + } + } + } + + emit layoutAboutToBeChanged(); + + // remove possible parent from new child + if (child->parentItem()) + { + beginRemoveRows(QModelIndex(), qMax(parent->childCount() - 1, 0), qMax(parent->childCount(), 0)); + child->parentItem()->removeChild(child); + endRemoveRows(); + } + + beginInsertRows(QModelIndex(), qMax(parent->childCount() - 1, 0), qMax(parent->childCount() - 1, 0)); + // set new parent in the child + child->setParentItem(parent); + // append child in parent's child list + parent->appendChild(child); + endInsertRows(); + + emit layoutChanged(); +} + +void ProblemTreeModel::removeItem( + ProblemTreeItem* item) +{ + if (!item) + { + return; + } + + emit layoutAboutToBeChanged(); + + if (item->parentItem()) + { + beginRemoveRows(QModelIndex(), qMax(item->childCount() - 1, 0), qMax(item->childCount(), 0)); + item->parentItem()->removeChild(item); + endRemoveRows(); + } + + emit layoutChanged(); +} + +ProblemTreeItem* ProblemTreeModel::rootItem() const +{ + return root_item_; +} + +QModelIndex ProblemTreeModel::rootIndex() +{ + return {}; +} + +int ProblemTreeModel::depth( + const QModelIndex& index) const +{ + int count = 0; + auto anchestor = index; + if (!index.isValid()) + { + return 0; + } + while (anchestor.parent().isValid()) + { + anchestor = anchestor.parent(); + ++count; + } + + return count; +} + +void ProblemTreeModel::clear() +{ + emit layoutAboutToBeChanged(); + beginResetModel(); + delete root_item_; + root_item_ = new ProblemTreeItem(); + endResetModel(); + emit layoutChanged(); +} + +ProblemTreeItem* ProblemTreeModel::internalPointer( + const QModelIndex& index) const +{ + return static_cast(index.internalPointer()); +} + +bool ProblemTreeModel::containsTopLevelItem( + ProblemTreeItem* child) +{ + if (child) + { + return contains(root_item_, child); + } + return false; +} + +bool ProblemTreeModel::contains( + ProblemTreeItem* parent, + ProblemTreeItem* child) +{ + if (!parent || !child) + { + return false; + } + + for (int i = 0; i < parent->childCount(); i++) + { + if (parent->child(i)->id() == child->id()) + { + return true; + } + } + return false; +} + +bool ProblemTreeModel::is_empty() +{ + return is_empty_; +} + +void ProblemTreeModel::removeEmptyItem() +{ + emit layoutAboutToBeChanged(); + for (int i=0; ichildCount(); i++) + { + if (root_item_->child(i)->id() == backend::ID_ALL) + { + root_item_->removeChild(root_item_->child(i)); + is_empty_ = false; + break; + } + } + emit layoutChanged(); +} + +ProblemTreeItem* ProblemTreeModel::getTopLevelItem( + const backend::EntityId& id, + const std::string& data, + const bool& is_error, + const std::string& description) +{ + // For each entity item in the three (root) + for (int i=0; ichildCount(); i++) + { + // if exists + if (root_item_->child(i)->id() == id) + { + return root_item_->child(i); + } + } + + // if not existing, create new topLevelItem + ProblemTreeItem* new_entity_item = new ProblemTreeItem(id, data, is_error, description); + addTopLevelItem(new_entity_item); + return new_entity_item; +} + + +QHash ProblemTreeModel::roleNames() const +{ + // TODO Jesus this roles are not currently used in the QML, find out why + QHash roles; + + roles[idRole] = "id"; + roles[statusRole] = "status"; + roles[kindRole] = "kind"; + roles[valueRole] = "value"; + roles[descriptionRole] = "description"; + roles[aliveRole] = "alive"; + roles[nameRole] = "name"; + + return roles; +} + +} // namespace models