diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f1808bc..859f0497 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -218,6 +218,8 @@ set(PROJECT_HEADERS ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/Engine.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/io/csv.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/io/ClipboardHandler.h + ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/alerts/AlertListItem.h + ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/alerts/AlertListModel.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/dds/EndpointModelItem.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/dds/LocatorModelItem.h ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}/model/dds/ParticipantModelItem.h @@ -256,6 +258,8 @@ set(PROJECT_SOURCES_NO_MAIN ${PROJECT_SOURCE_DIR}/src/Engine.cpp ${PROJECT_SOURCE_DIR}/src/io/csv.cpp ${PROJECT_SOURCE_DIR}/src/io/ClipboardHandler.cpp + ${PROJECT_SOURCE_DIR}/src/model/alerts/AlertListItem.cpp + ${PROJECT_SOURCE_DIR}/src/model/alerts/AlertListModel.cpp ${PROJECT_SOURCE_DIR}/src/model/dds/EndpointModelItem.cpp ${PROJECT_SOURCE_DIR}/src/model/dds/ParticipantModelItem.cpp ${PROJECT_SOURCE_DIR}/src/model/info/InfoModel.cpp diff --git a/docs/index.rst b/docs/index.rst index 8e5f2e1d..c4eadb80 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,8 +51,10 @@ /rst/user_manual/application_menu /rst/user_manual/shortcuts_bar /rst/user_manual/explorer_panel + /rst/user_manual/alerts_panel /rst/user_manual/status_panel /rst/user_manual/issues_panel + /rst/user_manual/alert_messages_panel /rst/user_manual/chart_panel_index /rst/user_manual/select_entity /rst/user_manual/export_data diff --git a/docs/rst/exports/alias.include b/docs/rst/exports/alias.include index 7f73cacb..37879de5 100644 --- a/docs/rst/exports/alias.include +++ b/docs/rst/exports/alias.include @@ -33,6 +33,10 @@ :width: 20 :alt: Clear Issues +.. |create_alert| image:: /rst/../../resources/images/icons/alert/alert_black.svg + :width: 20 + :alt: Create Alert + .. |resize| image:: /rst/../../resources/images/icons/resize/resize_grey.svg :width: 20 :alt: Reset Chart Zoom diff --git a/docs/rst/figures/screenshots/alert_messages_panel.png b/docs/rst/figures/screenshots/alert_messages_panel.png new file mode 100644 index 00000000..2eb41a29 Binary files /dev/null and b/docs/rst/figures/screenshots/alert_messages_panel.png differ diff --git a/docs/rst/figures/screenshots/alert_panel.png b/docs/rst/figures/screenshots/alert_panel.png new file mode 100644 index 00000000..afc4826a Binary files /dev/null and b/docs/rst/figures/screenshots/alert_panel.png differ diff --git a/docs/rst/figures/screenshots/usage_example/alert_dialog.png b/docs/rst/figures/screenshots/usage_example/alert_dialog.png new file mode 100644 index 00000000..36519b2a Binary files /dev/null and b/docs/rst/figures/screenshots/usage_example/alert_dialog.png differ diff --git a/docs/rst/figures/screenshots/usage_example/alert_panel_post.png b/docs/rst/figures/screenshots/usage_example/alert_panel_post.png new file mode 100644 index 00000000..afc4826a Binary files /dev/null and b/docs/rst/figures/screenshots/usage_example/alert_panel_post.png differ diff --git a/docs/rst/figures/screenshots/usage_example/alert_panel_pre.png b/docs/rst/figures/screenshots/usage_example/alert_panel_pre.png new file mode 100644 index 00000000..f887315a Binary files /dev/null and b/docs/rst/figures/screenshots/usage_example/alert_panel_pre.png differ diff --git a/docs/rst/getting_started/tutorial.rst b/docs/rst/getting_started/tutorial.rst index e7200146..0d6f0155 100644 --- a/docs/rst/getting_started/tutorial.rst +++ b/docs/rst/getting_started/tutorial.rst @@ -391,3 +391,33 @@ As you can see, the :code:`MEAN`, :code:`MAX` and :code:`MIN` in each interval a It is worth mentioning that dynamic series can be configurable, just like historic series. The label and color of each series is mutable, and the chart could zoom in and out and move along the axis while paused. + +Set alert to watch events +============================ + +This section describes how to create alerts to watch specific events in the monitored DDS network. First, click on +the *Alerts* tab (marked with a bell icon) in the left panel to open the Alerts view. In this tab, you can see a list +of all the defined alerts. + +.. thumbnail:: /rst/figures/screenshots/usage_example/alert_panel_pre.png + :align: center + +Click on the *+* button to create a new alert. This will open a dialog where you can configure the alert. + +.. thumbnail:: /rst/figures/screenshots/usage_example/alert_dialog.png + :align: center + +In this dialog, you can set the name of the alert, its type, the domain to monitor and the conditions for triggering the alert. + +If the alert type is *NEW_DATA*, the alert will be triggered when a positive `DATA_COUNT` is received from any entity that matches the fields +`host`, `user` and `topic`. Note that the statistics must be enabled for the entities to be able to trigger alerts. + +If the alert type is *NO_DATA*, the alert will be triggered when a `PUBLICATION_THROUGHPUT` message is received from any entity that matches +the fields `host`, `user` and `topic` and its value is lower than `threshold`. + +Once the alert is set up, it will appear in the list of alerts and its metadata will be shown below when clicked. + +.. thumbnail:: /rst/figures/screenshots/usage_example/alert_panel_post.png + :align: center + +To remove an alert, just right-click on it and choose the `Remove` option.` diff --git a/docs/rst/user_manual/alert_messages_panel.rst b/docs/rst/user_manual/alert_messages_panel.rst new file mode 100644 index 00000000..4827faf0 --- /dev/null +++ b/docs/rst/user_manual/alert_messages_panel.rst @@ -0,0 +1,12 @@ +.. include:: ../exports/alias.include +.. include:: ../exports/roles.include + +.. _alert_messages_panel: + +#################### +Alert Messages Panel +#################### + +This panel lists the alert messages of the application in a tree structure, where the messages +are grouped by the name of the alert that triggered them. In addition to the message, the +timestamp indicating when the alert was triggered is also shown. diff --git a/docs/rst/user_manual/alerts_panel.rst b/docs/rst/user_manual/alerts_panel.rst new file mode 100644 index 00000000..5b1c77ca --- /dev/null +++ b/docs/rst/user_manual/alerts_panel.rst @@ -0,0 +1,26 @@ +.. include:: ../exports/alias.include +.. include:: ../exports/roles.include + +.. _alerts_panel: + +############ +Alerts Panel +############ + +The alerts panel is located on the left side of the application window and is divided into two main sections: + +.. _alerts_list_panel: + +Alerts List +=========== + +This panel displays the list of alerts that have been triggered based on the conditions defined by the user. +When selected, the alert will be highlighted, and its details will be shown in the :ref:`alert_info_panel`. + +.. _alert_info_panel: + +Alert Info +========== + +This panel displays the specific information of the alert that is currently **selected** + diff --git a/docs/rst/user_manual/layout.rst b/docs/rst/user_manual/layout.rst index fc1ba6e5..5d16d750 100644 --- a/docs/rst/user_manual/layout.rst +++ b/docs/rst/user_manual/layout.rst @@ -146,6 +146,38 @@ This panel shows a summary of the main statistical data related with the last en For the explanation of this information refer to the section :ref:`statistics_panel`. +.. _alerts_panel_layout: + +Alerts Panel +============ + +This panel shows the different alerts created by the user to monitor specific events in the DDS network. + +.. figure:: /rst/figures/screenshots/alert_panel.png + :align: center + +For the explanation of this information refer to the section :ref:`alerts_panel`. + +.. _alert_list_layout: + +Alert List +---------- + +This panel lists the alerts created by the user to monitor specific events in the DDS network. +These alerts are created by clicking on the |create_alert| button in the Shortcuts Bar or in the +*+* symbol in the upper right corner of the panel. +Once created, the alerts will be listed in this panel, and the user can remove them by right clicking +on the alert and selecting the remove option. + +.. _alert_data_layout: + +Alert Data +---------- + +This panel shows the configuration values of the alert selected in the *Alert List*, including the +alert name, its domain, the values of host, user and topic of the monitored entities, its threshold +or the duration of the alert. + .. _monitor_status_panel_layout: Monitor Status Panel @@ -208,6 +240,17 @@ The events that the application reacts to in the current version are: For a thorough explanation of this information refer to the section :ref:`issues_panel`. +.. _alert_messages_panel_layout: + +Alert Messages Panel +==================== + +This panel lists the alert events that the application has detected based on the alerts created by the user. +These alerts are shown in a tree structure, where the most recent alerts are shown at the bottom. + +.. figure:: /rst/figures/screenshots/alert_messages_panel.png + :align: center + .. _main_panel_layout: Main Panel diff --git a/docs/rst/user_manual/shortcuts_bar.rst b/docs/rst/user_manual/shortcuts_bar.rst index f2e07498..3bb5c075 100644 --- a/docs/rst/user_manual/shortcuts_bar.rst +++ b/docs/rst/user_manual/shortcuts_bar.rst @@ -19,6 +19,7 @@ The meaning of each of the icons available in the shortcut bar is explained belo * |refresh| - Refresh Fast DDS Monitor. * |clear_log| - Clear the list of logs. * |clear_issues| - Clear the issues panel. +* |create_alert| - Open the create alerts panel. This bar can be hidden (or revealed in case it is already hidden) from the menu *View->Hide/Show Shortcuts Toolbar*. It is also possible to configure the shortcuts displayed from *View->Customize Shortcuts Toolbar*. diff --git a/fastdds_monitor.pro b/fastdds_monitor.pro index ea51e152..7d88afed 100644 --- a/fastdds_monitor.pro +++ b/fastdds_monitor.pro @@ -15,6 +15,8 @@ SOURCES += \ src/Engine.cpp \ src/io/csv.cpp \ src/main.cpp \ + src/model/alerts/AlertListModel.cpp \ + src/model/alerts/AlertListItem.cpp \ src/model/dds/EndpointModelItem.cpp \ src/model/dds/ParticipantModelItem.cpp \ src/model/info/InfoModel.cpp \ diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index e85e3d60..de68b792 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -123,6 +123,9 @@ public slots: //! Slot called when a Locator entity is pressed void locator_click( QString id); + //! Slot called when an Alert entity is pressed + void alert_click( + QString id); //! Slot called when refresh button is pressed void refresh_click(); @@ -229,6 +232,21 @@ public slots: QString new_alias, QString entity_kind); + //! Adds a new alert + void set_alert( + QString alert_name, + QString domain_name, + QString host_name, + QString user_name, + QString topic_name, + QString alert_type, + double threshold, + int time_between_triggers); + + //! Removes an alert + void remove_alert( + QString id); + //! Give a string with the name of the unit magnitud in which each DataKind is measured QString get_data_kind_units( QString data_kind); @@ -274,6 +292,9 @@ public slots: //! Retrive a string list containing the available data kinds. QStringList get_data_kinds(); + //! Retrive a string list containing the available alert kinds. + QStringList get_alert_kinds(); + //! Returns whether the data kind entered requires a target entity to be defined. bool data_kind_has_target( const QString& data_kind); diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index b77b757c..0e792b85 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -33,10 +33,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -336,6 +338,17 @@ class Engine : public QQmlApplicationEngine bool update_dds = true, bool reset_dds = true); + /** + * @brief Call the event chain when an alert is clicked + * + * For every alert it updates the summary model to reference this alert clicked. + * + * @param id Alert id of the alert clicked + * @return true if any change in any model has been done + */ + bool alert_clicked( + backend::AlertId id); + //! TODO bool on_selected_entity_kind( backend::EntityKind entity_kind, @@ -384,6 +397,19 @@ class Engine : public QQmlApplicationEngine bool add_callback( backend::StatusCallback callback); + /** + * @brief Add an alert callback arrived from the backend to the alert callback queue + * + * Add an alert callback to the alert 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 alert callback queue. + * Add an alert callback issue. + * + * @param callback new alert callback to add + * @return true + */ + bool add_callback( + backend::AlertCallback callback); + /** * @brief Refresh the view * @@ -434,6 +460,14 @@ class Engine : public QQmlApplicationEngine */ void process_status_callback_queue(); + /** + * @brief Pop alert callbacks from the alert 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_alert_callback_queue(); + //! Refresh summary panel void refresh_summary(); @@ -462,6 +496,21 @@ class Engine : public QQmlApplicationEngine const std::string& new_alias, const backend::EntityKind& entity_kind); + //! Set an alert + void set_alert( + const std::string& alert_name, + const backend::EntityId& domain_id, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, + const backend::AlertKind& alert_kind, + double threshold, + const std::chrono::milliseconds& t_between_triggers); + + //! Remove an alert + void remove_alert( + const backend::AlertId& id); + /** * This methods updates the info and summary if the entity clicked (the entity that is being shown) is the * entity updated. @@ -532,15 +581,18 @@ class Engine : public QQmlApplicationEngine const QString& file_name, bool clear); - //! Retrive a string vector containing the transport protocols supported by the Statistics Backend Discovery Server. + //! Retrieve a string vector containing the transport protocols supported by the Statistics Backend Discovery Server. std::vector ds_supported_transports(); - //! Retrive a string list containing the available statistic kinds. + //! Retrieve a string list containing the available statistic kinds. std::vector get_statistic_kinds(); - //! Retrive a string list containing the available data kinds. + //! Retrieve a string list containing the available data kinds. std::vector get_data_kinds(); + //! Retrieve a string list containing the available alert kinds. + std::vector get_alert_kinds(); + //! Retrieve the name associated to a specific entity std::string get_name( const backend::EntityId& entity_id); @@ -605,6 +657,12 @@ class Engine : public QQmlApplicationEngine */ void new_status_callback_signal(); + /** + * Internal signal that communicate that there are alert callbacks to process by the main Thread. + * Arise from \c add_callback + */ + void new_alert_callback_signal(); + public slots: /** @@ -619,6 +677,12 @@ public slots: */ void new_status_callback_slot(); + /** + * Receive the internal signal \c new_alert_callback_signal and start the process of alert + * callback queue by \c process_alert_callback_queue + */ + void new_alert_callback_slot(); + protected: /** @@ -674,11 +738,54 @@ public slots: */ bool fill_status_(); + /** + * @brief Update the alert summary model with an initial message that says there are + * no selected alerts + */ + bool fill_first_alert_summary_(); + + /** + * @brief Clear and fill the alert list model + * + * @return true if any change in any model has been done + */ + bool fill_alert_list_(); + + /** + * @brief Clear and fill the alert summary for a selected alert + * + * @param id id of the alert to get the info + * @return true if any change in any model has been done + */ + bool fill_alert_summary_( + backend::AlertId id); + + /** + * @brief Clears the alert summary + */ + bool clear_alert_summary_(); + + /** + * @brief Clear and fill the alert messages view + * + * @return true if any change in any model has been done + */ + bool fill_alert_message_(); + //! Add a new callback message to the Log model bool add_log_callback_( std::string callback, std::string time); + //! Updates the Alert model + bool update_alerts_(); + + //! Add a new alert message to the Alert Message model + bool add_alert_message_info_( + std::string alert_name, + std::string msg, + std::string time); + //! Add a new issue message to the Issue model bool add_issue_info_( std::string issue, @@ -689,6 +796,13 @@ public slots: std::string name, std::string time); + /** + * Generates a new alert message info model from the main schema + * The Alert Message model schema has: + * - "Alert Messages" tag - to collect alert messages + */ + void generate_new_alert_message_info_(); + /** * Generates a new issue info model from the main schema * The Issue model schema has: @@ -740,12 +854,18 @@ public slots: //! True if there are status callbacks in the callback queue bool are_status_callbacks_to_process_(); + //! True if there are alert callbacks in the callback queue + bool are_alert_callbacks_to_process_(); + //! Pop a callback from callback queues and call \c read_callback for that callback bool process_callback_(); //! Pop a status callback from callback queues and call \c read_callback for that status callback bool process_status_callback_(); + //! Pop an alert callback from callback queues and call \c read_callback for that alert callback + bool process_alert_callback_(); + //! Update the model concerned by the entity in the callback bool read_callback_( backend::Callback callback); @@ -754,6 +874,10 @@ public slots: bool read_callback_( backend::StatusCallback callback); + //! Update the model concerned by the entity in the alert callback + bool read_callback_( + backend::AlertCallback callback); + //! Common method to demultiplex to update functions depending on the entity kind bool update_entity_generic( backend::EntityId entity_id, @@ -767,6 +891,12 @@ public slots: //! Clear issues panel information void clear_issue_info_(); + //! Clear alerts info panel information + void clear_alert_info_(); + + //! Clear alert messages information + void clear_alert_message_info_(); + ///// // Variables @@ -797,6 +927,24 @@ public slots: //! Data that is represented in the Issue Model when this model is refreshed backend::Info issue_info_; + //! Data Model for Alerts. Collects alerts defined in the system + models::AlertListModel* alert_model_; + + //! Alert buffer + backend::Info alert_data_; + + //! Data Model for Info of the clicked alert + models::TreeModel* alerts_summary_model_; + + //! Information about the selected alert + backend::Info alert_info_; + + //! Data Model for Alert Messages. Collects alert messages and info from the whole system + models::TreeModel* alert_message_model_; + + //! Data that is represented in the Alert Message Model when this model is refreshed + backend::Info alert_message_info_; + //! Data Model for Log. Collects logging messages from application execution models::TreeModel* log_model_; @@ -821,6 +969,15 @@ public slots: //! TODO models::ListModel* destination_entity_id_model_; + //! Model to hold the data about the domains available for alert creation + models::ListModel* alert_domain_id_model_; + //! Model to hold the data about the hosts available for alert creation + models::ListModel* alert_host_id_model_; + //! Model to hold the data about the users available for alert creation + models::ListModel* alert_user_id_model_; + //! Model to hold the data about the topics available for alert creation + models::ListModel* alert_topic_id_model_; + //! Ids of the last Entity clicked EntitiesClicked last_entities_clicked_; @@ -845,12 +1002,18 @@ public slots: //! Mutex to protect \c status_callback_queue_ std::recursive_mutex status_callback_queue_mutex_; + //! Mutex to protect \c alert_callback_queue_ + std::recursive_mutex alert_callback_queue_mutex_; + //! Queue of Callbacks that have arrived by the \c Listener and have not been processed QQueue callback_queue_; //! Queue of status Callbacks that have arrived by the \c Listener and have not been processed QQueue status_callback_queue_; + //! Queue of alert Callbacks that have arrived by the \c Listener and have not been processed + QQueue alert_callback_queue_; + //! Object that manage all the communications with the QML view Controller* controller_; diff --git a/include/fastdds_monitor/backend/AlertCallback.h b/include/fastdds_monitor/backend/AlertCallback.h new file mode 100644 index 00000000..e0775d8d --- /dev/null +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -0,0 +1,49 @@ +// Copyright 2025 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 AlertCallback.h + */ + +#ifndef _EPROSIMA_FASTDDS_MONITOR_BACKEND_ALERT_CALLBACK_H +#define _EPROSIMA_FASTDDS_MONITOR_BACKEND_ALERT_CALLBACK_H + +#include +#include + + +namespace backend { + +enum AlertCallbackKind +{ + ALERT_TRIGGERED, + ALERT_UNMATCHED +}; + +struct AlertCallback +{ + AlertCallback() = default; + backend::EntityId domain_id; + backend::EntityId entity_id; + backend::AlertInfo alert_info; + std::string trigger_data; + AlertCallbackKind kind; +}; + +} // namespace backend + +#endif // _EPROSIMA_FASTDDS_MONITOR_BACKEND_ALERT_CALLBACK_H diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index be61fdd5..a9757730 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -97,6 +97,18 @@ class Listener : public PhysicalListener EntityId entity_id, StatusKind data_kind) override; + //! Callback when an alert is triggered + void on_alert_triggered( + EntityId domain_id, + EntityId entity_id, + AlertInfo& alert, + const std::string& data) override; + + //! Callback when an alert is unmatched + void on_alert_unmatched( + EntityId domain_id, + AlertInfo& alert) override; + protected: //! Engine reference diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 8309e137..2072a9d0 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -27,6 +27,8 @@ #include #include +#include +#include #include #include #include @@ -114,6 +116,10 @@ class SyncBackendConnection EntityInfo get_info( EntityId id); + //! Get alert info from an alert from the Backend + AlertSummary get_info( + backend::AlertId id); + //! Get the id of the topic associated to an endpoint backend::EntityId get_endpoint_topic_id( backend::EntityId endpoint_id); @@ -238,6 +244,9 @@ class SyncBackendConnection EntityKind entity_type, EntityId entity_id = EntityId::all()); + //! Get all alert ids from the Backend + std::vector get_alerts(); + //! Get the supported entity kinds of a given data kind std::vector> get_data_supported_entity_kinds( DataKind data_kind); @@ -294,6 +303,10 @@ class SyncBackendConnection ListItem* create_locator_data_( backend::EntityId id); + //! Create a new \c AlertListItem related with the backend alert with id \c id + AlertListItem* create_alert_data_( + backend::AlertId id); + /************ * GET DATA * ***********/ @@ -319,15 +332,18 @@ class SyncBackendConnection std::string get_data_kind_units( const DataKind data_kind); - //! Retrive a string vector containing the transport protocols supported by the Statistics Backend Discovery Server. + //! Retrieve a string vector containing the transport protocols supported by the Statistics Backend Discovery Server. std::vector ds_supported_transports(); - //! Retrive a string list containing the available statistic kinds. + //! Retrieve a string list containing the available statistic kinds. std::vector get_statistic_kinds(); - //! Retrive a string list containing the available data kinds. + //! Retrieve a string list containing the available data kinds. std::vector get_data_kinds(); + //! Retrieve a string list containing the available alert kinds. + std::vector get_alert_kinds(); + //! Returns whether the data kind entered requires a target entity to be defined. bool data_kind_has_target( const DataKind& data_kind); @@ -458,6 +474,15 @@ class SyncBackendConnection bool metatraffic_visible, bool proxy_visible); + /** + * @brief Update the alerts model with every alert in the backend + * + * @param alerts_model Alerts model to update + * @return true if any change has been made, false otherwise + */ + bool update_alerts_model( + models::AlertListModel* alerts_model); + ///// // Entity update functions @@ -639,6 +664,12 @@ class SyncBackendConnection bool metatraffic_visible, bool proxy_visible); + bool update_alert_item_( + AlertListItem* item); + + bool update_alert_item_info_( + AlertListItem* item); + /************** * UPDATE ONE * *************/ @@ -839,6 +870,21 @@ class SyncBackendConnection const backend::EntityId& id, const std::string& new_alias); + //! Set a new alert in backend + void set_alert( + const std::string& alert_name, + const EntityId& domain_id, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, + const backend::AlertKind& alert_kind, + double threshold, + const std::chrono::milliseconds& t_between_triggers); + + //! Remove an alert in backend + void remove_alert( + const backend::AlertId& id); + protected: ListModel* get_model_( diff --git a/include/fastdds_monitor/backend/backend_types.h b/include/fastdds_monitor/backend/backend_types.h index 13a256d7..1df185bc 100644 --- a/include/fastdds_monitor/backend/backend_types.h +++ b/include/fastdds_monitor/backend/backend_types.h @@ -25,6 +25,7 @@ #include +#include #include #include #include @@ -35,11 +36,15 @@ namespace backend { //! Add a type of each kind with same name under \c backend namespace using EntityId = eprosima::statistics_backend::EntityId; using EntityKind = eprosima::statistics_backend::EntityKind; +using EntityInfo = eprosima::statistics_backend::Info; using DataKind = eprosima::statistics_backend::DataKind; using StatusKind = eprosima::statistics_backend::StatusKind; using StatusLevel = eprosima::statistics_backend::StatusLevel; using StatisticKind = eprosima::statistics_backend::StatisticKind; -using EntityInfo = eprosima::statistics_backend::Info; +using AlertId = eprosima::statistics_backend::AlertId; +using AlertInfo = eprosima::statistics_backend::AlertInfo; +using AlertKind = eprosima::statistics_backend::AlertKind; +using AlertSummary = eprosima::statistics_backend::Info; using Timestamp = eprosima::statistics_backend::Timestamp; using GUID_s = eprosima::fastdds::statistics::detail::GUID_s; diff --git a/include/fastdds_monitor/backend/backend_utils.h b/include/fastdds_monitor/backend/backend_utils.h index f736c2fe..f8cab724 100644 --- a/include/fastdds_monitor/backend/backend_utils.h +++ b/include/fastdds_monitor/backend/backend_utils.h @@ -79,6 +79,18 @@ bool get_info_metatraffic( QString entity_kind_to_QString( const EntityKind& entity_kind); +//! Converts the \c AlertId in backend to models QString +models::AlertId alert_backend_id_to_models_id( + const AlertId& id); + +//! Converts the \c AlertId in models QString to backend AlertId +AlertId alert_models_id_to_backend_id( + const models::AlertId& id); + +//! Converts the \c AlertKind to QString +QString alert_kind_to_QString( + const AlertKind& alert_kind); + //! Converts the \c DataKind to string std::string data_kind_to_string( const DataKind& data_kind); @@ -107,6 +119,10 @@ backend::DataKind string_to_data_kind( backend::StatisticKind string_to_statistic_kind( const QString& statistic_kind); +//! Retrieves the \c AlertKind related with its name in QString +backend::AlertKind string_to_alert_kind( + const QString& alert_kind); + //! recursive function to convert array json subelements to dictionaries indexed by numbers backend::EntityInfo refactor_json( backend::EntityInfo json_data); diff --git a/include/fastdds_monitor/model/alerts/AlertListItem.h b/include/fastdds_monitor/model/alerts/AlertListItem.h new file mode 100644 index 00000000..eea2392b --- /dev/null +++ b/include/fastdds_monitor/model/alerts/AlertListItem.h @@ -0,0 +1,224 @@ +// Copyright 2025 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 AlertListItem.hpp + */ + +#ifndef _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTLISTITEM_H +#define _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTLISTITEM_H + +#include +#include +#include + +#include +#include +#include + +namespace models { + +class AlertListModel; + +/** + * @brief Abstract class that encapsulate the behaviour of alert Items in a model + * + * Each Item represents a different \c Alert in the backend. + * + * Implement the constructors for every Item with a backend \c AlertId . + * Implement main functions of every alert to retrieve internal data that is available in every alert kind, + * that is id, name and their roles associated. + */ +class AlertListItem : public QObject +{ + Q_OBJECT + +public: + + //! Role names to allow queries to get some specific information from the Item + enum ModelItemRoles + { + idRole = Qt::UserRole + 1, //! Role for attribute Id + kindRole, //! Role for attribute Kind + aliveRole, //! Role for attribute Alive + clickedRole, //! Role for attribute Clicked + 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) + }; + + //! Default QObject constructor. Used for model specification + AlertListItem( + QObject* parent = 0); + + //! Default QObject constructor. Used for model specification + AlertListItem( + backend::AlertId id, + QObject* parent = 0); + + //! Specific Item constructor, with a backend \c AlertId associated + AlertListItem( + backend::AlertId id, + backend::AlertSummary info, + QObject* parent = 0); + + //! AlertListItem destructor + ~AlertListItem(); + + /** + * @brief Item id getter + * + * Retrieve the Item id, that is the backend alert id in QString format. + * + * @return backend alert id + */ + AlertId alert_id() const; + + /** + * @brief Alert name getter + * + * This value is get with tag "name" in this item \c info + * + * @return alert name + */ + QString name() const; + + /** + * @brief Alert info getter + * @return alert info + */ + backend::AlertSummary info() const; + + /** + * @brief Alert kind getter + * @return alert kind in QString format + */ + QString kind() const; + + /** + * @brief Alert kind backend type getter + * @return alert kind + */ + backend::AlertKind backend_kind() const + { + return backend::AlertKind::INVALID_ALERT; + } + + /** + * @brief Alert alive status getter + * @return alert alive status + */ + bool alive() const; + + /** + * @brief Getter for clicked status + * @return alert clicked status + */ + bool clicked() const; + + /** + * @brief Item info setter + * + * Set the info to the Item. + * This is used when constructing a new alert, avoiding to call the backend in Item construction. + * + * @warning This method do not modify the backend information + * + * @param info new info + */ + void info( + backend::AlertSummary info) + { + info_ = info; + } + + /** + * @brief Item clicked setter + * + * Set the clicked status to the Item. + * + * @param clicked is last clicked + */ + void clicked( + bool clicked) + { + clicked_ = clicked; + } + + /** + * @brief Alert id getter + * + * Retrieve the backend id that reference to the alert encapsulated in this item. + * + * @return backend alert id + */ + backend::AlertId get_alert_id() const; + + /** + * @brief General getter to return every internal value + * + * This method allows to retrieve every internal Qt information from the Item. + * This is needed for the QML method to print the alert information. + * + * In Abstract AlertListItem it allows to get the id and name of the alert. + * Override this method in subclasses with different values in order to be able to access to all of them. + * + * @param role value of \c ModelItemRoles enumeration that references an internal value + * + * @return value of the role for this item + */ + virtual QVariant data( + int role) const; + + /** + * @brief Roles getter + * + * Returns all the possible roles that are implemented for this object. + * By this values, it is possible to get the internal data by calling \c data method. + * + * @return role names + */ + virtual QHash roleNames() const; + + /** + * @brief Emit a signal that indicates that the Item has been modified + */ + virtual void triggerItemUpdate() + { + emit dataChanged(); + } + +signals: + + //! Communicate to the view that some internal info has been modified/added/removed/updated + void dataChanged(); + +protected: + + //! Backend Id that references the \c Alert that this Item represents + backend::AlertId id_; + + //! Backend info that contains all the needed information for this item + backend::AlertSummary info_; + + //! States whether the alert is clicked or not + bool clicked_; +}; + +} // namespace models + +#endif // _EPROSIMA_FASTDDS_MONITOR_MODEL_AlertListItem_H diff --git a/include/fastdds_monitor/model/alerts/AlertListModel.h b/include/fastdds_monitor/model/alerts/AlertListModel.h new file mode 100644 index 00000000..917dde9e --- /dev/null +++ b/include/fastdds_monitor/model/alerts/AlertListModel.h @@ -0,0 +1,207 @@ +// Copyright 2025 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 AlertListModel.hpp + */ + +#ifndef _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTLISTMODEL_H +#define _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTLISTMODEL_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace models { + +/** + * @brief Abstract class that encapsulate the behaviour of a Model that contains Entities in the form of Items + * + * The \c AlertListModel contains a serie of \c AlertListItem , which each of them represents a backend \c Alert . + * It implements the main methods to interact with the view. + * + * These models are the graphic representation for the backed Alerts, storing internally the items that represent + * each Alert, its information and its state. + * They also manage the interaction between the view, the controller and the items. + */ +class AlertListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + +public: + + /** + * Constructor. + */ + explicit AlertListModel( + AlertListItem* prototype, + QObject* parent = 0); + + /** + * Destructor. + */ + ~AlertListModel(); + + /** + * Returns the number of rows under the given parent. + * When the parent is valid it means that rowCount is returning the number of children of parent. + */ + int rowCount( + const QModelIndex& parent = QModelIndex()) const Q_DECL_OVERRIDE; + + /** + * Returns the data stored under the given role for the item referred to by the index. + */ + QVariant data( + const QModelIndex& index, + int role) const Q_DECL_OVERRIDE; + + /** + * Returns the model's role names. + */ + QHash roleNames() const Q_DECL_OVERRIDE; + + /** + * This function provides a convenient way to append a single new item. + */ + void appendRow( + AlertListItem* item); + + /** + * Appends several items to the Model. + */ + void appendRows( + QList& items); + + /** + * Inserts a single row before the given row in the child items of the parent specified. + */ + void insertRow( + int row, + AlertListItem* item); + + /** + * Removes a single row at the position given by row. + * Returns true if the row was removed, and false if the row was not found or is not valid. + */ + bool removeRow( + int row, + const QModelIndex& index = QModelIndex()); + + /** + * Removes several rows starting at position given by row until either count or the model's last row is reached. + * Returns true if the rows were removed, false if the given initial row was not fount or is not invalid. + */ + bool removeRows( + int row, + int count, + const QModelIndex& index = QModelIndex()) Q_DECL_OVERRIDE; + + /** + * Returns the item whose id matches the models itemId. + */ + AlertListItem* find( + AlertId itemId) const; + + /** + * Returns the item whose id matches the backend itemId. + * + * @note This method is faster with backend than models id as the actual value stored is \c backend::AlertId + */ + AlertListItem* find( + backend::AlertId itemId) const; + + /** + * Returns the item whose id matches the itemId. + */ + AlertListItem* at( + int index) const; + + /** + * Returns the row index of item in the model. + */ + int getRowFromItem( + AlertListItem* item) const; + + /** + * Returns the index of the row in the model containing the item. + */ + QModelIndex indexFromItem( + AlertListItem* item) const; + + /** + * Returns a QList containing the items of the model. + */ + QList to_QList() const; + + /** + * Returns a QVariant containg the data of the row item at a given index in the model. + */ + Q_INVOKABLE QVariant get( + int index); + + /** + * Returns the row index of an item given the models item id. + */ + Q_INVOKABLE int rowIndexFromId( + AlertId itemId); + + /** + * Returns the row index of an item given the backend item id. + */ + Q_INVOKABLE int rowIndexFromId( + backend::AlertId itemId); + + /** + * Clears the whole model removing all rows. + */ + Q_INVOKABLE void clear(); + +protected: + + //! Void AlertListItem that is used to know the role names. This is why a default constructor is needed in \c AlertListItem + AlertListItem* prototype_; + + // This object stores the subitems indexing by (internal list index) that is used by qt. + // Thus, it is not easy to create a map with item and alert in a simple way. + //! List of Items under this model + QList items_; + +private slots: + + /** + * Slot triggered when a row item needs to be updated to reflect data changes. + */ + void updateItem(); + +signals: + + //! Signal that communicates to the view that the number of items has changed + void countChanged( + int count); +}; + +} // namespace models + +#endif // _EPROSIMA_FASTDDS_MONITOR_MODEL_LISTMODEL_H diff --git a/include/fastdds_monitor/model/model_types.h b/include/fastdds_monitor/model/model_types.h index 318cc515..5bc8430d 100644 --- a/include/fastdds_monitor/model/model_types.h +++ b/include/fastdds_monitor/model/model_types.h @@ -28,6 +28,8 @@ namespace models { //! The type of EntityId within the models is a QString came by the conversion of backend \c EntityId into string using EntityId = QString; +//! The type of AlertId within the models is a QString came by the conversion of backend \c AlertId into string +using AlertId = QString; //! Reference the EntityId::all() in models constexpr const char* ID_ALL = "all"; diff --git a/include/fastdds_monitor/model/tree/TreeItem.h b/include/fastdds_monitor/model/tree/TreeItem.h index 7b561ad3..3ca9562e 100644 --- a/include/fastdds_monitor/model/tree/TreeItem.h +++ b/include/fastdds_monitor/model/tree/TreeItem.h @@ -63,6 +63,9 @@ class TreeItem TreeItem* child_item( int row); + void remove_child_item( + int row); + //! Count the number of children int child_count() const; diff --git a/include/fastdds_monitor/model/tree/TreeModel.h b/include/fastdds_monitor/model/tree/TreeModel.h index e5dd2d71..d529a0fa 100644 --- a/include/fastdds_monitor/model/tree/TreeModel.h +++ b/include/fastdds_monitor/model/tree/TreeModel.h @@ -111,6 +111,10 @@ class TreeModel : public QAbstractItemModel void update( json data); + //! Clear the model and create a new tree with new data without collapsing the view + void update_without_collapse( + json& data); + //! Return the role names of the values in nodes to acces them via \c data QHash roleNames() const Q_DECL_OVERRIDE; @@ -146,6 +150,29 @@ class TreeModel : public QAbstractItemModel TreeItem* parent, bool _first = true); + /** + * @brief Recursive function that fills an internal node with data in json format without + * collapsing the view + * + * @param parent Item from which the update is performed + * @param parent_index Index of the parent in the TreeModel + * @param json_data Data with the new version of the tree + */ + void setup_model_data_without_collapse( + TreeItem* parent, + const QModelIndex& parent_index, + const json& json_data); + + /** + * @brief Iterates over a node to find a child with a specific name + * @param parent parent node where to search + * @param name name of the child node to search + * @return pointer to the child node if found, nullptr otherwise + */ + TreeItem* find_child_by_name( + TreeItem* parent, + const QString& name) const; + private: //! Parent node of the items tree diff --git a/mock/complex_mock/StatisticsBackend.cpp b/mock/complex_mock/StatisticsBackend.cpp index c8c093a7..ca6634ad 100644 --- a/mock/complex_mock/StatisticsBackend.cpp +++ b/mock/complex_mock/StatisticsBackend.cpp @@ -155,6 +155,12 @@ std::vector StatisticsBackend::get_entities( return Database::get_instance()->get_entities(entity_type, entity_id); } +// Get the alerts vector +std::vector StatisticsBackend::get_alerts() +{ + return Database::get_instance()->get_alerts_ids(); +} + // Returns the EntityKind of the entity with id entity_id EntityKind StatisticsBackend::get_type( EntityId entity_id) @@ -328,6 +334,45 @@ void StatisticsBackend::set_alias( Database::get_instance()->set_alias(entity_id, alias); } +void StatisticsBackend::set_alert( + const std::string& alert_name, + const EntityId& domain_id, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, + const AlertKind& alert_kind, + const double& threshold, + const std::chrono::milliseconds& t_between_triggers) +{ + switch (alert_kind) + { + case AlertKind::NEW_DATA_ALERT: + { + NewDataAlertInfo new_data_alert(alert_name, domain_id, host_name, user_name, topic_name, + t_between_triggers); + Database::get_instance()->insert_alert(new_data_alert); + } + break; + case AlertKind::NO_DATA_ALERT: + { + NoDataAlertInfo no_data_alert(alert_name, domain_id, host_name, user_name, topic_name, threshold, + t_between_triggers); + Database::get_instance()->insert_alert(no_data_alert); + } + break; + // Handle other alert kinds as needed + case AlertKind::INVALID_ALERT: + default: + return; + } +} + +void StatisticsBackend::remove_alert( + const AlertId& alert_id) +{ + Database::get_instance()->remove_alert(alert_id); +} + bool StatisticsBackend::is_active( EntityId entity_id) { diff --git a/mock/complex_mock/database/Database.cpp b/mock/complex_mock/database/Database.cpp index 1dffb6ee..2e3f54a8 100644 --- a/mock/complex_mock/database/Database.cpp +++ b/mock/complex_mock/database/Database.cpp @@ -414,5 +414,45 @@ bool Database::get_active( } } +AlertId Database::insert_alert( + AlertInfo& alert_info) +{ + AlertId id = next_alert_id_++; + alert_info.set_id(id); + alerts_[alert_info.get_domain_id()].emplace(id, std::make_shared(alert_info)); + return id; +} + +void Database::remove_alert( + const AlertId& alert_id) +{ + // Iterate over all domains as ID is unique but domain is not known + for (auto& domain_it : alerts_) + { + auto alert_it = domain_it.second.find(alert_id); + if (alert_it != domain_it.second.end()) + { + // If alert is found in this domain, it is removed + domain_it.second.erase(alert_it); + return; + } + } +} + +std::vector Database::get_alerts_ids() const +{ + std::vector alertsIds; + + for (const auto& domain_it: alerts_) + { + for (const auto& alert_it : domain_it.second) + { + alertsIds.push_back(alert_it.first); + } + } + + return alertsIds; +} + } // namespace statistics_backend } // namespace eprosima diff --git a/mock/complex_mock/database/Database.hpp b/mock/complex_mock/database/Database.hpp index b22ca4b2..e750cda2 100644 --- a/mock/complex_mock/database/Database.hpp +++ b/mock/complex_mock/database/Database.hpp @@ -151,6 +151,28 @@ class Database bool get_active( EntityId id); + /** + * @brief Setter for entity alert. + * + * @param alert_info The new alert information. + * @return The AlertId of the alert. + */ + AlertId insert_alert( + AlertInfo& alert_info); + + /** + * @brief Remove an alert from the database. + * + * @param alert_info The alert id + */ + void remove_alert( + const AlertId& alert_id); + + /** + * @brief Gets the lists of active alerts + */ + std::vector get_alerts_ids() const; + protected: /** @@ -201,6 +223,11 @@ class Database //! Store all the entities by key \c EntityId and value \c EntityPointer to the entity std::map entities_; + //! Store all the alerts by key \c EntityId of the domain and value a map of key \c AlertId and value pointer to the alert info + std::map>> alerts_; + //! The ID that will be assigned to the next alert. + std::atomic next_alert_id_{0}; + /** * Store the callbacks that will be sent by the callback thread. * Each tuple is formed by: < New element Id , Kind , Domain Id > diff --git a/qml.qrc b/qml.qrc index 064c48a4..8a533803 100644 --- a/qml.qrc +++ b/qml.qrc @@ -16,6 +16,11 @@ qml/AboutDialog.qml qml/AdaptiveComboBox.qml qml/AdaptiveMenu.qml + qml/AlertDialog.qml + qml/AlertList.qml + qml/AlertsMenu.qml + qml/AlertsPanel.qml + qml/AlertSummary.qml qml/ChangeAliasDialog.qml qml/ChartsLayout.qml qml/CustomLegend.qml @@ -81,6 +86,10 @@ resources/images/loading_graph.gif + resources/images/icons/alert/alert_black.svg + resources/images/icons/alert/alert_eProsimaLightBlue.svg + resources/images/icons/alert/alert_grey.svg + resources/images/icons/alert/alert_white.svg resources/images/icons/clearissues/clearissues_black.svg resources/images/icons/clearissues/clearissues_eProsimaLightBlue.svg resources/images/icons/clearissues/clearissues_grey.svg diff --git a/qml/AlertDialog.qml b/qml/AlertDialog.qml new file mode 100644 index 00000000..0d69e5f0 --- /dev/null +++ b/qml/AlertDialog.qml @@ -0,0 +1,555 @@ +// Copyright 2025 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.6 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 +import Theme 1.0 + +Dialog { + id: alertDialog + + readonly property int layout_vertical_spacing_: 10 // vertical spacing between the components in a row + readonly property int layout_horizontal_spacing_: 15 // horizontal spacing between rows + readonly property int item_height_: 40 // Height of header item and each item of + // advanced options submenu (title + options) + readonly property int dialog_width_: 300 // Width of the dialog + + property var availableAlertKinds: [] + property bool activeOk: true + property string currentKind: "" + property string currentAlertName: "" + property string currentDomain: "" + property string currentHost: "" + property string currentUser: "" + property string currentTopic: "" + property double currentThreshold: 0 + property int currentTimeBetweenAlerts: 5000 + + modal: false + title: "Add alert" + standardButtons: Dialog.Ok | Dialog.Cancel + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + signal createAlert(string alert_name, string domain_name, string host_name, string user_name, + string topic_name, string alert_type, int t_between_triggers, int threshold) + + Component.onCompleted: { + availableAlertKinds = controller.get_alert_kinds() + standardButton(Dialog.Ok).text = qsTrId("Add") + standardButton(Dialog.Cancel).text = qsTrId("Close") + } + + onAccepted: { + if (!checkInputs()) + return + + currentKind = alertKindComboBox.currentText + currentAlertName = alertNameTextField.text + currentDomain = domainComboBox.currentText + currentHost = manualHostCheckBox.checked ? manualHostText.text : hostComboBox.currentText + currentUser = manualUserCheckBox.checked ? manualUserText.text : userComboBox.currentText + currentTopic = manualTopicCheckBox.checked ? manualTopicText.text : topicComboBox.currentText + currentTimeBetweenAlerts = alertTimeBetweenAlerts.value + currentThreshold = parseFloat(alertThreshold.text) + + createAlert(currentAlertName, currentDomain, currentHost, currentUser, currentTopic, currentKind, currentTimeBetweenAlerts, currentThreshold) + } + + onAboutToShow: { + alertKindComboBox.currentIndex = -1 + alertNameTextField.text = "" + domainComboBox.currentIndex = -1 + hostComboBox.currentIndex = -1 + topicComboBox.currentIndex = -1 + userComboBox.currentIndex = -1 + updateDomains() + updateTopics() + updateUsers() + updateHosts() + } + + GridLayout{ + + columns: 2 + rowSpacing: 20 + + Label { + id: alertKindLabel + text: "Alert kind: " + InfoToolTip { + text: "Type of alert to be created." + } + } + AdaptiveComboBox { + id: alertKindComboBox + displayText: currentIndex === -1 + ? ("Please choose an alert kind...") + : currentText + model: availableAlertKinds + + Component.onCompleted: currentIndex = -1 + onActivated: { + activeOk = true + } + } + + Label { + id: alertNameLabel + text: "Alert name: " + InfoToolTip { + text: "Name of the alert.\n"+ + "The alert name is autogerated\n" + + "using the values given in this\n" + + "dialog." + } + } + + TextField { + id: alertNameTextField + placeholderText: "" + selectByMouse: true + maximumLength: 100 + Layout.fillWidth: true + + onTextEdited: activeOk = true + } + + Label { + id: domainLabel + text: "Domain: " + InfoToolTip { + text: "Domain watched by the alert." + } + } + + AdaptiveComboBox { + id: domainComboBox + enabled: true + textRole: "nameId" + valueRole: "id" + popup.y: height + displayText: currentIndex === -1 + ? ("Please choose a domain...") + : currentText + model: alertDomainModel + Component.onCompleted: currentIndex = -1 + + onActivated: { + activeOk = true + } + } + + Label { + id: hostLabel + text: "Host: " + InfoToolTip { + text: "Host watched by the alert." + } + } + + AdaptiveComboBox { + id: hostComboBox + enabled: !manualHostCheckBox.checked + textRole: "nameId" + valueRole: "id" + popup.y: height + displayText: currentIndex === -1 + ? ("Please choose a host...") + : currentText + model: alertHostModel + Component.onCompleted: currentIndex = -1 + + onActivated: { + activeOk = true + } + } + + Label { + id: userLabel + text: "User: " + InfoToolTip { + text: "User watched by the alert." + } + } + + AdaptiveComboBox { + id: userComboBox + enabled: !manualUserCheckBox.checked + textRole: "nameId" + valueRole: "id" + popup.y: height + displayText: currentIndex === -1 + ? ("Please choose a user...") + : currentText + model: alertUserModel + Component.onCompleted: currentIndex = -1 + + onActivated: { + activeOk = true + } + } + + Label { + id: topicLabel + text: "Topic: " + InfoToolTip { + text: "Topic watched by the alert." + } + } + + AdaptiveComboBox { + id: topicComboBox + enabled: !manualTopicCheckBox.checked + textRole: "nameId" + valueRole: "id" + popup.y: height + displayText: currentIndex === -1 + ? ("Please choose a topic...") + : currentText + model: alertTopicModel + Component.onCompleted: currentIndex = -1 + + onActivated: { + activeOk = true + } + } + + Label { + text: "Threshold: " + InfoToolTip { + text: "Threshold of the throughput under which the alert will start triggering." + } + } + + TextField { + id: alertThreshold + enabled: alertKindComboBox.currentText !== "NEW_DATA" + Connections { + target: alertKindComboBox + onCurrentTextChanged: { + if (alertKindComboBox.currentText === "NEW_DATA") { + alertThreshold.text = "0" + } + } + } + } + + Label { + text: "Time between alerts (ms): " + InfoToolTip { + text: "Minimum time between two consecutive alerts." + } + } + SpinBox { + id: alertTimeBetweenAlerts + editable: true + from: 0 + to: 10000 + stepSize: 50 + value: 5000 + } + + Rectangle { + id: advancedOptionsSubmenu + color: "transparent" + + property bool isExpanded: false + readonly property int item_height_: alertDialog.item_height_ + readonly property int collapsed_options_box_height_: item_height_ + readonly property int options_box_body_height_: item_height_ + readonly property int expanded_options_box_height_: collapsed_options_box_height_ + 3*options_box_body_height_ + + Layout.fillWidth: true + Layout.preferredHeight: isExpanded + ? expanded_options_box_height_ + 20 + : collapsed_options_box_height_ + 20 + + Column { + anchors.fill: parent + spacing: layout_horizontal_spacing_ + RowLayout { + width: parent.width + + Rectangle { + Layout.fillWidth: true + height: 2 + color: Theme.lightGrey + } + + Rectangle { + width: 175 + height: 30 + radius: 15 + color: advancedOptionsMouseArea.containsMouse ? Theme.grey : "transparent" + Row { + anchors.verticalCenter: parent.verticalCenter + spacing: alertDialog.layout_vertical_spacing_ + leftPadding: 10 + + IconSVG { + anchors.verticalCenter: parent.verticalCenter + name: advancedOptionsSubmenu.isExpanded ? "cross" : "plus" + size: 12 + color: advancedOptionsMouseArea.containsMouse ? "white" : "grey" + } + + Label { + anchors.verticalCenter: parent.verticalCenter + text: "Advanced options" + color: advancedOptionsMouseArea.containsMouse ? "white" : "grey" + } + } + + MouseArea { + id: advancedOptionsMouseArea + width: parent.width + height: parent.height + hoverEnabled: true + onClicked: { + advancedOptionsSubmenu.isExpanded = !advancedOptionsSubmenu.isExpanded + } + } + } + } + + GridLayout{ + + columns: 2 + rows: 3 + rowSpacing: 20 + width: parent.width + visible: advancedOptionsSubmenu.isExpanded + height: advancedOptionsSubmenu.isExpanded ? advancedOptionsSubmenu.item_height_ : 0 + // spacing: alertDialog.layout_horizontal_spacing_ + + + CheckBox { + id: manualHostCheckBox + text: "Set host name manually" + checked: false + + indicator: Rectangle { + implicitWidth: 16 + implicitHeight: 16 + anchors.verticalCenter: parent.verticalCenter + border.color: Theme.grey + border.width: 2 + Rectangle { + visible: manualHostCheckBox.checked + color: Theme.eProsimaLightBlue + radius: 1 + anchors.margins: 3 + anchors.fill: parent + } + } + } + + TextField { + id: manualHostText + enabled: manualHostCheckBox.checked + selectByMouse: true + placeholderText: "manual_host_name" + width: 130 + + background: Rectangle { + color: !manualHostCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } + + onTextChanged: { + } + } + + CheckBox { + id: manualUserCheckBox + text: "Set user name manually" + checked: false + + indicator: Rectangle { + implicitWidth: 16 + implicitHeight: 16 + anchors.verticalCenter: parent.verticalCenter + border.color: Theme.grey + border.width: 2 + Rectangle { + visible: manualUserCheckBox.checked + color: Theme.eProsimaLightBlue + radius: 1 + anchors.margins: 3 + anchors.fill: parent + } + } + } + + TextField { + id: manualUserText + enabled: manualUserCheckBox.checked + selectByMouse: true + placeholderText: "manual_user_name" + width: 130 + + background: Rectangle { + color: !manualUserCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } + + onTextChanged: { + } + } + + CheckBox { + id: manualTopicCheckBox + text: "Set topic name manually" + checked: false + + indicator: Rectangle { + implicitWidth: 16 + implicitHeight: 16 + anchors.verticalCenter: parent.verticalCenter + border.color: Theme.grey + border.width: 2 + Rectangle { + visible: manualTopicCheckBox.checked + color: Theme.eProsimaLightBlue + radius: 1 + anchors.margins: 3 + anchors.fill: parent + } + } + } + + TextField { + id: manualTopicText + enabled: manualTopicCheckBox.checked + selectByMouse: true + placeholderText: "manual_topic_name" + width: 130 + + background: Rectangle { + color: !manualTopicCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } + + onTextChanged: { + } + } + } + } + } + } + + MessageDialog { + id: emptyAlertKind + title: "Missing alert kind" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The alert kind field is empty. Please enter an alert kind." + onAccepted: alertDialog.open() + onDiscard: alertDialog.close() + } + + + MessageDialog { + id: emptyAlertName + title: "Missing alert name" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The alert name field is empty. Please enter an alert name." + onAccepted: alertDialog.open() + onDiscard: alertDialog.close() + } + + MessageDialog { + id: emptyAlertDomain + title: "Missing domain" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The domain field is empty. Please enter a domain." + onAccepted: alertDialog.open() + onDiscard: alertDialog.close() + } + + function checkInputs() { + if (alertKindComboBox.currentIndex === -1) { + emptyAlertKind.open() + return false + } + if (alertNameTextField.text === "") { + emptyAlertName.open() + return false + } + if (domainComboBox.currentIndex === -1) { + emptyAlertDomain.open() + return false + } + + return true + } + + function updateDomains() { + controller.update_available_entity_ids("Domain", "alertDomain") + domainComboBox.recalculateWidth() + } + + function updateTopics() { + controller.update_available_entity_ids("Topic", "alertTopic") + topicComboBox.recalculateWidth() + } + + function updateUsers(){ + controller.update_available_entity_ids("User", "alertUser") + userComboBox.recalculateWidth() + } + + function updateHosts(){ + controller.update_available_entity_ids("Host", "alertHost") + hostComboBox.recalculateWidth() + } + + function abbreviateEntityName(entityName){ + return entityName.split(":")[0] + } + + function regenerateAlertName(){ + alertNameTextField.text = "" + if (manualHostCheckBox.checked && manualHostText.text !== "") { + alertNameTextField.text += abbreviateEntityName(manualHostText.text) + } else if (hostComboBox.currentIndex !== -1) { + alertNameTextField.text += abbreviateEntityName(hostComboBox.currentText) + } + + if (manualUserCheckBox.checked && manualUserText.text !== "") { + alertNameTextField.text += "_" + abbreviateEntityName(manualUserText.text) + } else if (userComboBox.currentIndex !== -1) { + alertNameTextField.text += "_" + abbreviateEntityName(userComboBox.currentText) + } + + if (manualTopicCheckBox.checked && manualTopicText.text !== "") { + alertNameTextField.text += "_" + abbreviateEntityName(manualTopicText.text) + } else if (topicComboBox.currentIndex !== -1) { + alertNameTextField.text += "_" + abbreviateEntityName(topicComboBox.currentText) + } + + if (alertKindComboBox.currentIndex !== -1) { + alertNameTextField.text += "_" + alertKindComboBox.currentText + } + } +} diff --git a/qml/AlertList.qml b/qml/AlertList.qml new file mode 100644 index 00000000..db9caee2 --- /dev/null +++ b/qml/AlertList.qml @@ -0,0 +1,102 @@ +// Copyright 2025 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 QtQml.Models 2.15 +import Theme 1.0 + + +Rectangle { + + id: alertListRect + Layout.fillHeight: true + Layout.fillWidth: true + + property int verticalSpacing: 5 + property int spacingIconLabel: 8 + property int iconSize: 18 + property int firstIndentation: 5 + property int secondIndentation: firstIndentation + iconSize + spacingIconLabel + property int thirdIndentation: secondIndentation + iconSize + spacingIconLabel + + ListView { + id: alertList + model: alertModel + delegate: alertListDelegate + clip: true + anchors.fill : parent + spacing: verticalSpacing + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.vertical: CustomScrollBar { + id: scrollBar + } + } + + Component { + id: alertListDelegate + + Item { + id: alertItem + width: alertListRect.width + height: alertHighlightRect.height + property var alertId: id + property int alertIdx: index + + Rectangle { + + id: alertHighlightRect + width: alertList.width + height: alertIcon.height + color: clicked ? Theme.eProsimaLightBlue : "transparent" + property bool clicked : false + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: { + alertHighlightRect.clicked = !alertHighlightRect.clicked + if(mouse.button & Qt.RightButton) { + openAlertsMenu(id) + } else { + controller.alert_click(id) + } + } + } + + RowLayout { + spacing: spacingIconLabel + + IconSVG { + id: alertIcon + name: "alert" + size: iconSize + Layout.leftMargin: firstIndentation + color: entityLabelColor(alertHighlightRect.clicked, alive) + } + Label { + text: name + color: entityLabelColor(alertHighlightRect.clicked, alive) + } + } + } + } + } +} diff --git a/qml/AlertSummary.qml b/qml/AlertSummary.qml new file mode 100644 index 00000000..928894f6 --- /dev/null +++ b/qml/AlertSummary.qml @@ -0,0 +1,68 @@ +// Copyright 2025 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.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Window 2.2 +import QtQml.Models 2.11 + +Item { + id: alert_summary_view + visible: true + + TreeView { + id: alert_summary_tree_view + anchors.fill: parent + model: alertsSummaryModel + frameVisible: false + selectionMode: SelectionMode.NoSelection + selection: ItemSelectionModel { + id: item_selection_model + model: alertsSummaryModel + } + itemDelegate: Item { + Text { + anchors.fill: parent + elide: styleData.elideMode + text: styleData.value + } + } + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + + TableViewColumn { + width: parent.width / 2 + role: "name" + title: "Name" + } + + TableViewColumn { + width: parent.width / 2 + role: "value" + title: "Value" + } + + Component.onCompleted: leftPanel.expandAll(alert_summary_tree_view, alertsSummaryModel) + + Connections { + target: alertsSummaryModel + function onUpdatedData() { + leftPanel.expandAll(alert_summary_tree_view, alertsSummaryModel) + } + } + } +} diff --git a/qml/AlertsMenu.qml b/qml/AlertsMenu.qml new file mode 100644 index 00000000..e53f3326 --- /dev/null +++ b/qml/AlertsMenu.qml @@ -0,0 +1,40 @@ +// Copyright 2025 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 Theme 1.0 + +/* + Menu containing the possible actions that can be performed on any Alert + */ +Menu { + id: alertsMenu + + property string alertId: "" + + signal removeAlert() + + ////////////////// + // Menu options // + ////////////////// + + MenuItem { + text: "Remove Alert" + onTriggered: alertsMenu.removeAlert(alertsMenu.alertId) + } +} diff --git a/qml/AlertsPanel.qml b/qml/AlertsPanel.qml new file mode 100644 index 00000000..b6039ae5 --- /dev/null +++ b/qml/AlertsPanel.qml @@ -0,0 +1,139 @@ +// Copyright 2025 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.6 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.12 +import Theme 1.0 + +/* + Sidebar containing the Alerts + */ +ColumnLayout { + id: alertsPanel + spacing: 0 + + Rectangle { + Layout.fillWidth: true + height: 20 + color: Theme.grey + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + anchors.leftMargin: 5 + anchors.rightMargin: 5 + Label { + text: "Alerts" + Layout.preferredWidth: parent.width - parent.height + color: Theme.whiteSmoke + } + IconSVG { + name: "plus" + Layout.alignment: Qt.AlignRight + scalingFactor: 1.4 + color: "white" + + MouseArea { + anchors.fill: parent + + onClicked: { + alertDialog.open() + } + } + } + } + } + + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + + SplitView { + orientation: Qt.Vertical + anchors.fill: parent + + ColumnLayout { + id: alertListLayout + SplitView.preferredHeight: parent.height / 4 + SplitView.minimumHeight: alertListTitle.height + spacing: 10 + visible: true + clip: true + + Rectangle { + id: alertListTitle + Layout.fillWidth: true + height: infoTabBar.height + Label { + text: "Alert List" + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + } + Rectangle { + color: Theme.eProsimaLightBlue + height: 2 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + } + } + + AlertList { + id: alertList + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.bottomMargin: 1 + } + } + + Item { + id: alertInfoLayout + visible: true + SplitView.fillHeight: true + SplitView.preferredHeight: parent.height / 4 + SplitView.minimumHeight: infoTabBar.height + clip: true + + TabBar { + id: infoTabBar + anchors.top: parent.top + anchors.left: parent.left + width: parent.width + TabButton { + text: qsTr("Info") + } + } + + StackLayout { + currentIndex: infoTabBar.currentIndex + anchors.top: infoTabBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + width: parent.width + + AlertSummary { + id: alertSummaryView + } + } + } + } + } +} diff --git a/qml/EntitiesMenu.qml b/qml/EntitiesMenu.qml index c472bd43..459499ce 100644 --- a/qml/EntitiesMenu.qml +++ b/qml/EntitiesMenu.qml @@ -13,19 +13,19 @@ Menu { property string entityId: "" property string currentAlias: "" property string entityKind: "" - property string showGraphButtonName: "" + property string showGraphButtonName: "" signal changeAlias(string domainEntityId, string entityId, string currentAlias, string entityKind) signal filterEntityStatusLog(string entityId) signal openTopicView(string domainEntityId, string domainId, string topicId) - + ////////////////// // Menu options // ////////////////// Component { id: changeAlias - + MenuItem { text: "Change alias" MouseArea { @@ -86,12 +86,12 @@ Menu { ListModel { id: entityModel } - + Repeater { model: entityModel delegate: Loader { sourceComponent: available ? option : null - } + } } // Update model if some property change implies graphic changes in UI diff --git a/qml/IconsVBar.qml b/qml/IconsVBar.qml index e09e43d8..b9b06956 100644 --- a/qml/IconsVBar.qml +++ b/qml/IconsVBar.qml @@ -22,8 +22,8 @@ import QtQml.Models 2.12 import Theme 1.0 /* - Object to create the sidebar for shortcuts to DDS entity lists and information, monitor status and issues. - The accesses are arranged in a list containing the icons of each drop-down. + Object to create the sidebar for shortcuts to DDS entity lists and information, alerts, + monitor status and issues. The accesses are arranged in a list containing the icons of each drop-down. Each item in the list displays its corresponding display sidebar. */ @@ -41,6 +41,9 @@ Rectangle { ListElement { icon: "explorer" } + ListElement { + icon: "alert" + } ListElement { icon: "status" } diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index 295561a8..71c38236 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -28,6 +28,7 @@ RowLayout { enum LeftSubPanel { Explorer, + Alerts, Status, Issues } @@ -38,7 +39,7 @@ RowLayout { domainGraph: 1 }) - property variant panelItem: [monitoringPanel, statusPanel, issuesPanel] + property variant panelItem: [monitoringPanel, alertsPanel, statusPanel, issuesPanel] property variant visiblePanel: panelItem[LeftPanel.LeftSubPanel.Explorer] @@ -50,6 +51,7 @@ RowLayout { signal open_idl_view(string entityId) signal refresh_domain_graph_view(string domainEntityId, string entityId) signal filter_entity_status_log(string entityId) + signal remove_alert(string alertId) MonitoringPanel { id: monitoringPanel @@ -67,6 +69,12 @@ RowLayout { visible: (visiblePanel === panelItem[LeftPanel.LeftSubPanel.Status]) ? true : false } + AlertsPanel { + id: alertsPanel + Layout.fillHeight: true + visible: (visiblePanel === panelItem[LeftPanel.LeftSubPanel.Alerts]) ? true : false + } + ChangeAliasDialog { id: aliasDialog } @@ -76,7 +84,12 @@ RowLayout { onChangeAlias: leftPanel.changeAlias(domainEntityId, entityId, currentAlias, entityKind) onFilterEntityStatusLog: leftPanel.filterEntityStatusLog(entityId) onOpenTopicView: leftPanel.openTopicView(domainEntityId, domainId, topicId) - } + } + + AlertsMenu { + id: alertsMenu + onRemoveAlert: leftPanel.removeAlert(alertsMenu.alertId) + } TopicMenu { id: topicMenu @@ -112,6 +125,11 @@ RowLayout { entitiesMenu.popup() } + function openAlertsMenu(alertId) { + alertsMenu.alertId = alertId + alertsMenu.popup() + } + function openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) { topicMenu.domainEntityId = domainEntityId topicMenu.domainId = domainId @@ -188,4 +206,12 @@ RowLayout { function changeExplorerEntityInfo(status) { monitoringPanel.changeExplorerEntityInfo(status) } + + function createAlert(name, domainId, hostId, userId, topicId, alert_type, threshold, t_between_triggers) { + controller.set_alert(name, domainId, hostId, userId, topicId, alert_type, threshold, t_between_triggers); + } + + function removeAlert(alertId) { + controller.remove_alert(alertId) + } } diff --git a/qml/MonitorMenuBar.qml b/qml/MonitorMenuBar.qml index 45d0a528..c9ae1d64 100644 --- a/qml/MonitorMenuBar.qml +++ b/qml/MonitorMenuBar.qml @@ -10,6 +10,7 @@ MenuBar { signal initMonitorButtonHidden signal dispDataButtonHidden signal dispDynDataButtonHidden + signal createAlertButtonHidden signal refreshButtonHidden signal clearLogButtonHidden signal clearIssuesButtonHidden @@ -74,6 +75,10 @@ MenuBar { text: qsTr("Display Real-&Time Data") onTriggered: dynamicDataKindDialog.open() } + Action { + text: qsTr("Create Alert") + onTriggered: alertDialog.open() + } MenuSeparator { } Action { text: qsTr("Delete inactive entities") @@ -351,6 +356,16 @@ MenuBar { Label { text: "Display Real-Time Data" } + CheckBox { + id: createAlertCheckBox + checked: false + indicator.width: 20 + indicator.height: 20 + onCheckStateChanged: createAlertButtonHidden() + } + Label { + text: "Create Alert" + } CheckBox { id: refreshCheckBox checked: true diff --git a/qml/MonitorToolBar.qml b/qml/MonitorToolBar.qml index a58f5a94..e6b2fa6a 100644 --- a/qml/MonitorToolBar.qml +++ b/qml/MonitorToolBar.qml @@ -27,6 +27,7 @@ ToolBar { property bool isVisible: false property bool isVisibleDispData: false property bool isVisibleDispDynData: true + property bool isVisibleCreateAlert: false property bool isVisibleRefresh: true property bool isVisibleClearLog: false property bool isVisibleClearIssues: false @@ -76,6 +77,14 @@ ToolBar { onClicked: dynamicDataKindDialog.open() } + MonitorToolBarButton { + id: createAlert + iconName: "alert" + tooltipText: "Create alert" + visible: isVisibleCreateAlert + onClicked: alertDialog.open() + } + MonitorToolBarButton { id: refresh iconName: "refresh" diff --git a/qml/Panels.qml b/qml/Panels.qml index cf8c0e6a..f3e571d2 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -101,6 +101,7 @@ RowLayout { onRefresh_domain_graph_view: tabs.refresh_domain_graph_view(domainEntityId, entityId) onFilter_entity_status_log: statusLayout.filter_entity_status_log(entityId) onOpen_idl_view: tabs.open_idl_view(entityId) + onRemove_alert: panels.removeAlert(alertId) } Rectangle { @@ -138,6 +139,9 @@ RowLayout { onOpenTopicMenu: { panels.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } + onOpenAlertsMenu: { + panels.openAlertsMenu(alertId) + } } StatusLayout { id: statusLayout @@ -206,4 +210,16 @@ RowLayout { function openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) { leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } + + function openAlertsMenu(alertId) { + leftPanel.openAlertsMenu(alertId) + } + + function createAlert(name, domainId, hostId, userId, topicId, alert_type, threshold, t_between_triggers){ + leftPanel.createAlert(name, domainId, hostId, userId, topicId, alert_type, threshold, t_between_triggers) + } + + function removeAlert(alertId){ + leftPanel.removeAlert(alertId) + } } diff --git a/qml/StatusLayout.qml b/qml/StatusLayout.qml index beca6d9f..911b8b09 100644 --- a/qml/StatusLayout.qml +++ b/qml/StatusLayout.qml @@ -99,6 +99,48 @@ Item } } + // Main Alerts tab + Tab { + title: "Alerts" + Rectangle { + + color: "white" + + // Main content of alerts tab: alert tree view with alerts per entity + TreeView { + id: alertMessagesView + anchors.fill: parent + model: alertMessageModel + selectionMode: SelectionMode.NoSelection + frameVisible: false + itemDelegate: Item { + Text { + anchors.fill: parent + elide: styleData.elideMode + text: { + // Error when undefined value. + // Do not know when this could happen, but happens + styleData.value ? styleData.value : "" + } + } + } + + TableViewColumn { + width: parent.width / 2 + role: "name" + title: "Name" + } + + TableViewColumn { + width: parent.width / 2 + role: "value" + title: "Value" + } + } + } + } + + // Tab main stlye style: TabViewStyle { frameOverlap: 1 @@ -246,7 +288,7 @@ Item color: Theme.grey } - // footer (and ALWAYS displayed) error and warning counters bar section + // footer (and ALWAYS displayed) error, warning and alerts counters bar section Rectangle { id: icon_section anchors.bottom: parent.bottom @@ -289,6 +331,21 @@ Item anchors.verticalCenter: parent.verticalCenter text: "0" } + IconSVG { + id: alert_icon + anchors.left: warning_value.right + anchors.leftMargin: elements_spacing_ + anchors.verticalCenter: parent.verticalCenter + name: "alert" + size: parent.height - elements_spacing_ + } + Label { + id: alert_value + anchors.left: alert_icon.right + anchors.leftMargin: elements_spacing_/2 + anchors.verticalCenter: parent.verticalCenter + text: "0" + } Connections { diff --git a/qml/TabLayout.qml b/qml/TabLayout.qml index f2fc9818..6cfedf58 100644 --- a/qml/TabLayout.qml +++ b/qml/TabLayout.qml @@ -31,6 +31,7 @@ Item { // Public signals signal openEntitiesMenu(string domainEntityId, string entityId, string currentAlias, string entityKind, int caller) signal openTopicMenu(string domainEntityId, string domainId, string entityId, string currentAlias, string entityKind, int caller) + signal openAlertsMenu(string alertId) // Private properties property int current_: 0 // current tab displayed diff --git a/qml/TopicMenu.qml b/qml/TopicMenu.qml index f2a320e9..38c012ea 100644 --- a/qml/TopicMenu.qml +++ b/qml/TopicMenu.qml @@ -47,4 +47,11 @@ Menu { text: "Data type IDL view" onTriggered: openIDLView(menu.entityId) } + MenuItem { + text: "Set Alert" + onTriggered: { + alertDialog.open() + } + } } + diff --git a/qml/main.qml b/qml/main.qml index 80527888..c507e364 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -55,6 +55,7 @@ ApplicationWindow { onToolBarHidden: toolBar.isVisible = !toolBar.isVisible onDispDataButtonHidden: toolBar.isVisibleDispData = !toolBar.isVisibleDispData onDispDynDataButtonHidden: toolBar.isVisibleDispDynData = !toolBar.isVisibleDispDynData + onCreateAlertButtonHidden: toolBar.isVisibleCreateAlert = !toolBar.isVisibleCreateAlert onRefreshButtonHidden: toolBar.isVisibleRefresh = !toolBar.isVisibleRefresh onClearLogButtonHidden: toolBar.isVisibleClearLog = !toolBar.isVisibleClearLog onDashboardLayoutButtonHidden: toolBar.isVisibleDashboardLayout = !toolBar.isVisibleDashboardLayout @@ -119,6 +120,13 @@ ApplicationWindow { onCreateChart: panels.createDynamicChart(dataKind, timeWindowSeconds, updatePeriod, maxPoints) } + AlertDialog { + id: alertDialog + onCreateAlert: { + panels.createAlert(alert_name, domain_name, host_name, user_name, topic_name, alert_type, threshold, t_between_triggers) + } + } + ScheduleClearDialog { id: scheduleClear } diff --git a/resources/images/icons/alert/alert_black.svg b/resources/images/icons/alert/alert_black.svg new file mode 100644 index 00000000..cb7bca54 --- /dev/null +++ b/resources/images/icons/alert/alert_black.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/images/icons/alert/alert_eProsimaLightBlue.svg b/resources/images/icons/alert/alert_eProsimaLightBlue.svg new file mode 100644 index 00000000..3acf5677 --- /dev/null +++ b/resources/images/icons/alert/alert_eProsimaLightBlue.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/icons/alert/alert_grey.svg b/resources/images/icons/alert/alert_grey.svg new file mode 100644 index 00000000..68fb78dd --- /dev/null +++ b/resources/images/icons/alert/alert_grey.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/icons/alert/alert_white.svg b/resources/images/icons/alert/alert_white.svg new file mode 100644 index 00000000..63b37639 --- /dev/null +++ b/resources/images/icons/alert/alert_white.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controller.cpp b/src/Controller.cpp index bb90072b..5d0c1c04 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -108,6 +108,12 @@ void Controller::locator_click( engine_->entity_clicked(backend::models_id_to_backend_id(id), backend::EntityKind::LOCATOR); } +void Controller::alert_click( + QString id) +{ + engine_->alert_clicked(backend::alert_models_id_to_backend_id(id)); +} + void Controller::update_available_entity_ids( QString entity_kind, QString entity_model_id) @@ -295,6 +301,47 @@ void Controller::set_alias( backend::string_to_entity_kind(entity_kind)); } +std::string clean_entity_name( + std::string original_name) +{ + size_t pos = original_name.find(':'); + if (pos != std::string::npos) + { + return original_name.substr(0, pos); + } + return original_name; +} + +void Controller::set_alert( + QString alert_name, + QString domain_id, + QString host_name, + QString user_name, + QString topic_name, + QString alert_type, + double threshold, + int time_between_triggers) +{ + std::string clean_host_name = clean_entity_name(utils::to_string(host_name)); + std::string clean_user_name = clean_entity_name(utils::to_string(user_name)); + std::string clean_topic_name = clean_entity_name(utils::to_string(topic_name)); + + engine_->set_alert(utils::to_string(alert_name), + backend::models_id_to_backend_id(domain_id), + clean_host_name, + clean_user_name, + clean_topic_name, + backend::string_to_alert_kind(alert_type), + threshold, + std::chrono::milliseconds(time_between_triggers)); +} + +void Controller::remove_alert( + QString id) +{ + engine_->remove_alert(backend::alert_models_id_to_backend_id(id)); +} + QString Controller::get_data_kind_units( QString data_kind) { @@ -342,6 +389,11 @@ QStringList Controller::get_data_kinds() return utils::to_QStringList(engine_->get_data_kinds()); } +QStringList Controller::get_alert_kinds() +{ + return utils::to_QStringList(engine_->get_alert_kinds()); +} + bool Controller::data_kind_has_target( const QString& data_kind) { diff --git a/src/Engine.cpp b/src/Engine.cpp index a9f6e6c7..c6d9e788 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -37,6 +37,8 @@ #include #include #include +#include +#include #include #include #include @@ -91,6 +93,18 @@ QObject* Engine::enable() generate_new_log_info_(); fill_log_(); + // Creates a default json structure for alerts and fills the tree model with it + alert_model_ = new models::AlertListModel(new models::AlertListItem()); + fill_alert_list_(); + + alerts_summary_model_ = new models::TreeModel(); + fill_first_alert_summary_(); + + // Creates a default json structure for statuses and fills the tree model with it + alert_message_model_ = new models::TreeModel(); + generate_new_alert_message_info_(); + fill_alert_message_(); + // Creates a default json structure for status messages and fills the tree model with it status_model_ = new models::TreeModel(); generate_new_status_info_(); @@ -104,12 +118,20 @@ QObject* Engine::enable() entity_status_proxy_model_ = new models::StatusTreeModel(); entity_status_proxy_model_->set_source_model(entity_status_model_); - 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()); fill_available_entity_id_list_(backend::EntityKind::HOST, "getDataDialogDestinationEntityId"); + alert_domain_id_model_ = new models::ListModel(new models::EntityItem()); + fill_available_entity_id_list_(backend::EntityKind::DOMAIN, "alertDomain"); + alert_host_id_model_ = new models::ListModel(new models::EntityItem()); + fill_available_entity_id_list_(backend::EntityKind::HOST, "alertHost"); + alert_user_id_model_ = new models::ListModel(new models::EntityItem()); + fill_available_entity_id_list_(backend::EntityKind::USER, "alertUser"); + alert_topic_id_model_ = new models::ListModel(new models::EntityItem()); + fill_available_entity_id_list_(backend::EntityKind::TOPIC, "alertTopic"); + historic_statistics_data_ = new HistoricStatisticsData(); dynamic_statistics_data_ = new DynamicStatisticsData(); @@ -131,9 +153,16 @@ QObject* Engine::enable() rootContext()->setContextProperty("logModel", log_model_); rootContext()->setContextProperty("statusModel", status_model_); rootContext()->setContextProperty("entityStatusModel", entity_status_proxy_model_); + rootContext()->setContextProperty("alertModel", alert_model_); + rootContext()->setContextProperty("alertMessageModel", alert_message_model_); + rootContext()->setContextProperty("alertsSummaryModel", alerts_summary_model_); rootContext()->setContextProperty("entityModelFirst", source_entity_id_model_); rootContext()->setContextProperty("entityModelSecond", destination_entity_id_model_); + rootContext()->setContextProperty("alertDomainModel", alert_domain_id_model_); + rootContext()->setContextProperty("alertHostModel", alert_host_id_model_); + rootContext()->setContextProperty("alertTopicModel", alert_topic_id_model_); + rootContext()->setContextProperty("alertUserModel", alert_user_id_model_); rootContext()->setContextProperty("historicData", historic_statistics_data_); rootContext()->setContextProperty("dynamicData", dynamic_statistics_data_); @@ -159,6 +188,12 @@ QObject* Engine::enable() this, &Engine::new_status_callback_slot); + QObject::connect( + this, + &Engine::new_alert_callback_signal, + this, + &Engine::new_alert_callback_slot); + // Set enable as True enabled_ = true; @@ -228,6 +263,21 @@ Engine::~Engine() delete entity_status_proxy_model_; } + if (alert_model_) + { + delete alert_model_; + } + + if (alerts_summary_model_) + { + delete alerts_summary_model_; + } + + if (alert_message_model_) + { + delete alert_message_model_; + } + // Auxiliar models if (source_entity_id_model_) { @@ -239,6 +289,23 @@ Engine::~Engine() delete destination_entity_id_model_; } + if (alert_domain_id_model_) + { + delete alert_domain_id_model_; + } + if (alert_host_id_model_) + { + delete alert_host_id_model_; + } + if (alert_user_id_model_) + { + delete alert_user_id_model_; + } + if (alert_topic_id_model_) + { + delete alert_topic_id_model_; + } + // Interactive models if (historic_statistics_data_) { @@ -402,6 +469,39 @@ bool Engine::fill_issue_() return true; } +bool Engine::fill_first_alert_summary_() +{ + EntityInfo info = R"({"No alerts active.":"Start an alert in a specific domain"})"_json; + alerts_summary_model_->update(info); + return true; +} + +bool Engine::fill_alert_list_() +{ + alert_model_->clear(); + return backend_connection_.update_alerts_model(alert_model_); +} + +bool Engine::fill_alert_summary_( + backend::AlertId id) +{ + alerts_summary_model_->update(backend_connection_.get_info(id)); + return true; +} + +bool Engine::clear_alert_summary_() +{ + EntityInfo info = R"({})"_json; + alerts_summary_model_->update(info); + return true; +} + +bool Engine::fill_alert_message_() +{ + alert_message_model_->update_without_collapse(alert_message_info_); + return true; +} + bool Engine::fill_log_() { log_model_->update(log_info_); @@ -414,6 +514,13 @@ bool Engine::fill_status_() return true; } +void Engine::generate_new_alert_message_info_() +{ + EntityInfo info; + + alert_message_info_ = info; +} + void Engine::generate_new_issue_info_() { EntityInfo info; @@ -493,6 +600,28 @@ void Engine::clear_issue_info_() fill_issue_(); } +bool Engine::update_alerts_() +{ + fill_alert_list_(); + return true; +} + +bool Engine::add_alert_message_info_( + std::string alert_name, + std::string msg, + std::string time) +{ + alert_message_info_[alert_name][time] = msg; + fill_alert_message_(); + return true; +} + +void Engine::clear_alert_message_info_() +{ + alert_message_info_ = EntityInfo(); + fill_alert_message_(); +} + bool Engine::fill_first_entity_info_() { EntityInfo info = R"({"No monitors active.":"Start a monitor in a specific domain"})"_json; @@ -735,6 +864,15 @@ bool Engine::entity_clicked( return res; } +bool Engine::alert_clicked( + backend::AlertId id) +{ + qDebug() << "Clicked alert: "; + bool res = false; + res = fill_alert_summary_(id) || res; + return res; +} + bool Engine::fill_available_entity_id_list_( backend::EntityKind entity_kind, QString entity_model_id) @@ -766,6 +904,46 @@ bool Engine::on_selected_entity_kind( metatraffic_visible(), proxy_visible()); } + else if (entity_model_id == "alertDomain") + { + alert_domain_id_model_->clear(); + return backend_connection_.update_get_data_dialog_entity_id( + alert_domain_id_model_, + backend::EntityKind::DOMAIN, + inactive_visible(), + metatraffic_visible(), + proxy_visible()); + } + else if (entity_model_id == "alertHost") + { + alert_host_id_model_->clear(); + return backend_connection_.update_get_data_dialog_entity_id( + alert_host_id_model_, + backend::EntityKind::HOST, + inactive_visible(), + metatraffic_visible(), + proxy_visible()); + } + else if (entity_model_id == "alertUser") + { + alert_user_id_model_->clear(); + return backend_connection_.update_get_data_dialog_entity_id( + alert_user_id_model_, + backend::EntityKind::USER, + inactive_visible(), + metatraffic_visible(), + proxy_visible()); + } + else if (entity_model_id == "alertTopic") + { + alert_topic_id_model_->clear(); + return backend_connection_.update_get_data_dialog_entity_id( + alert_topic_id_model_, + backend::EntityKind::TOPIC, + inactive_visible(), + metatraffic_visible(), + proxy_visible()); + } else { return false; @@ -787,7 +965,8 @@ QtCharts::QVXYModelMapper* Engine::on_add_statistics_data_series( backend::Timestamp time_from = start_time_default ? initial_time_ : backend::Timestamp(std::chrono::milliseconds(start_time)); backend::Timestamp time_to = - end_time_default ? std::chrono::system_clock::now() : backend::Timestamp(std::chrono::milliseconds(end_time)); + end_time_default ? std::chrono::system_clock::now() : + backend::Timestamp(std::chrono::milliseconds(end_time)); std::vector statistic_data = backend_connection_.get_data( data_kind, @@ -839,6 +1018,7 @@ void Engine::refresh_engine( fill_physical_data_(); fill_logical_data_(); + fill_alert_list_(); if (!maintain_clicked) { @@ -935,6 +1115,15 @@ void Engine::process_status_callback_queue() } } +void Engine::process_alert_callback_queue() +{ + // It iterates while run_ is activate and the queue has elements + while (!alert_callback_queue_.empty()) + { + process_alert_callback_(); + } +} + bool Engine::are_callbacks_to_process_() { std::lock_guard ml(callback_queue_mutex_); @@ -947,6 +1136,12 @@ bool Engine::are_status_callbacks_to_process_() return status_callback_queue_.empty(); } +bool Engine::are_alert_callbacks_to_process_() +{ + std::lock_guard ml(alert_callback_queue_mutex_); + return alert_callback_queue_.empty(); +} + bool Engine::add_callback( backend::Callback callback) { @@ -971,6 +1166,18 @@ bool Engine::add_callback( return true; } +bool Engine::add_callback( + backend::AlertCallback alert_callback) +{ + std::lock_guard ml(alert_callback_queue_mutex_); + alert_callback_queue_.append(alert_callback); + + // Emit signal to specify there are new data + emit new_alert_callback_signal(); + + return true; +} + void Engine::new_callback_slot() { process_callback_queue(); @@ -981,6 +1188,11 @@ void Engine::new_status_callback_slot() process_status_callback_queue(); } +void Engine::new_alert_callback_slot() +{ + process_alert_callback_queue(); +} + bool Engine::process_callback_() { backend::Callback first_callback; @@ -1011,6 +1223,21 @@ bool Engine::process_status_callback_() return read_callback_(first_status_callback); } +bool Engine::process_alert_callback_() +{ + backend::AlertCallback first_alert_callback; + + { + std::lock_guard ml(alert_callback_queue_mutex_); + first_alert_callback = alert_callback_queue_.front(); + alert_callback_queue_.pop_front(); + } + + qDebug() << "Processing alert callback: " << backend::backend_id_to_models_id(first_alert_callback.entity_id); + + return read_callback_(first_alert_callback); +} + bool Engine::read_callback_( backend::Callback callback) { @@ -1055,6 +1282,39 @@ bool Engine::read_callback_( return update_entity_status(status_callback.entity_id, status_callback.status_kind); } +bool Engine::read_callback_( + backend::AlertCallback alert_callback) +{ + // It should not read callbacks while a domain is being initialized + std::lock_guard lock(initializing_monitor_); + if (alert_callback.kind == backend::AlertCallbackKind::ALERT_UNMATCHED) + { + return add_alert_message_info_(alert_callback.alert_info.get_alert_name(), + "Alert unmatched: there are no entities that meet the alert conditions", utils::now()); + } + + // Add callback to log model + switch (alert_callback.alert_info.get_alert_kind()) + { + case backend::AlertKind::NEW_DATA_ALERT: + return add_alert_message_info_( + alert_callback.alert_info.get_alert_name(), + "New data received, DATA_COUNT is " + alert_callback.trigger_data, utils::now()); + break; + case backend::AlertKind::NO_DATA_ALERT: + return add_alert_message_info_( + alert_callback.alert_info.get_alert_name(), + "SUBSCRIPTION_THROUGHPUT is " + alert_callback.trigger_data, utils::now()); + break; + case backend::AlertKind::INVALID_ALERT: + default: + // Unknown alerts are ignored + break; + } + + return false; +} + bool Engine::update_entity_status( const backend::EntityId& id, backend::StatusKind kind) @@ -1246,7 +1506,8 @@ bool Engine::update_entity_status( std::string( "Check for compatible rules ") + std::string( - "here"), @@ -1258,7 +1519,8 @@ bool Engine::update_entity_status( { EntityInfo entity_info = backend_connection_.get_info(remote_entity_id); std::string remote_entity_kind = utils::to_string( - backend::entity_kind_to_QString(backend_connection_.get_type(remote_entity_id))); + backend::entity_kind_to_QString(backend_connection_.get_type( + remote_entity_id))); std::stringstream ss; ss << std::string(entity_info["alias"]) << " (" << remote_entity_kind << ")"; remote_entity = ss.str(); @@ -1673,6 +1935,32 @@ void Engine::set_alias( } } +void Engine::set_alert( + const std::string& alert_name, + const backend::EntityId& domain_id, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, + const backend::AlertKind& alert_kind, + double threshold, + const std::chrono::milliseconds& t_between_triggers) +{ + // Adding alert to backend structures + backend_connection_.set_alert(alert_name, domain_id, host_name, user_name, topic_name, alert_kind, threshold, + t_between_triggers); + // Update the list of alerts without using the refresh button + update_alerts_(); +} + +void Engine::remove_alert( + const backend::AlertId& id) +{ + backend_connection_.remove_alert(id); + // Update the list of alerts without using the refresh button + update_alerts_(); + clear_alert_summary_(); +} + bool Engine::update_entity( const backend::EntityId& entity_updated, bool (Engine::* update_function)(const backend::EntityId&, bool, bool), @@ -1708,6 +1996,7 @@ void Engine::change_inactive_visible() fill_physical_data_(); fill_logical_data_(); fill_dds_data_(); + fill_alert_list_(); refresh_engine(); } @@ -1717,6 +2006,7 @@ void Engine::change_metatraffic_visible() fill_physical_data_(); fill_logical_data_(); fill_dds_data_(); + fill_alert_list_(); refresh_engine(); } @@ -1726,6 +2016,7 @@ void Engine::change_ros2_demangling() fill_physical_data_(); fill_logical_data_(); fill_dds_data_(); + fill_alert_list_(); refresh_engine(); } @@ -1847,6 +2138,11 @@ std::vector Engine::get_data_kinds() return backend_connection_.get_data_kinds(); } +std::vector Engine::get_alert_kinds() +{ + return backend_connection_.get_alert_kinds(); +} + std::string Engine::get_name( const backend::EntityId& entity_id) { diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index 26b57379..c4caa0a0 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -177,4 +177,30 @@ void Listener::on_status_reported( engine_->add_callback(StatusCallback(domain_id, entity_id, status_kind)); } +void Listener::on_alert_triggered( + EntityId domain_id, + EntityId entity_id, + AlertInfo& alert, + const std::string& data) +{ + AlertCallback callback; + callback.domain_id = domain_id; + callback.entity_id = entity_id; + callback.alert_info = alert; + callback.trigger_data = data; + callback.kind = AlertCallbackKind::ALERT_TRIGGERED; + engine_->add_callback(callback); +} + +void Listener::on_alert_unmatched( + EntityId domain_id, + AlertInfo& alert) +{ + AlertCallback callback; + callback.domain_id = domain_id; + callback.alert_info = alert; + callback.kind = AlertCallbackKind::ALERT_UNMATCHED; + engine_->add_callback(callback); +} + } //namespace backend diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 96576899..f181c88b 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -117,6 +117,13 @@ ListItem* SyncBackendConnection::create_locator_data_( return new LocatorModelItem(id, get_info(id)); } +AlertListItem* SyncBackendConnection::create_alert_data_( + backend::AlertId id) +{ + qDebug() << "Creating Alert " << backend::backend_id_to_models_id(id); + return new AlertListItem(id, get_info(id)); +} + /// UPDATE PRIVATE FUNCTIONS bool SyncBackendConnection::update_host_item( ListItem* host_item, @@ -326,6 +333,55 @@ bool SyncBackendConnection::update_dds_model( proxy_visible); } +bool SyncBackendConnection::update_alert_item_( + AlertListItem* item) +{ + bool res = update_alert_item_info_(item); + return res; +} + +bool SyncBackendConnection::update_alert_item_info_( + AlertListItem* item) +{ + // Query for this item info and update it + item->info(get_info(item->get_alert_id())); + item->triggerItemUpdate(); + return true; +} + +bool SyncBackendConnection::update_alerts_model( + AlertListModel* alerts_model) +{ + bool changed = false; + + // For each User get all processes + for (auto& alert_id : get_alerts()) + { + // Check if it exists already + int index = alerts_model->rowIndexFromId(alert_id); + + // If it does not exist, it creates it and add a Row with it + // If it exists it updates its info + if (index == -1) + { + // Creates the Item object and update its data + alerts_model->appendRow(create_alert_data_(alert_id)); + changed = true; + models::AlertListItem* alert_item = alerts_model->find(alert_id); + + changed = update_alert_item_(alert_item) || changed; + } + else + { + // Otherwise just update the entity + models::AlertListItem* alert_item = alerts_model->at(index); + changed = update_alert_item_(alert_item) || changed; + } + } + + return changed; +} + bool SyncBackendConnection::update_get_data_dialog_entity_id( models::ListModel* entity_model, EntityKind entity_kind, @@ -553,6 +609,22 @@ EntityInfo SyncBackendConnection::get_info( } } +AlertSummary SyncBackendConnection::get_info( + AlertId id) +{ + try + { + return backend::refactor_json(StatisticsBackend::get_info(id)); + } + catch (const Exception& e) + { + qWarning() << "Fail getting entity info: " << e.what(); + static_cast(e); // In release qWarning does not compile and so e is not used + + return EntityInfo(); + } +} + backend::EntityId SyncBackendConnection::get_endpoint_topic_id( backend::EntityId endpoint_id) { @@ -662,6 +734,21 @@ std::vector SyncBackendConnection::get_entities( } } +std::vector SyncBackendConnection::get_alerts() +{ + try + { + return StatisticsBackend::get_alerts(); + } + catch (const Exception& e) + { + qWarning() << "Fail getting alerts: " << e.what(); + static_cast(e); // In release qWarning does not compile and so e is not used + + return std::vector(); + } +} + EntityInfo SyncBackendConnection::get_summary( backend::EntityId id) { @@ -1222,6 +1309,42 @@ void SyncBackendConnection::set_alias( } } +void SyncBackendConnection::set_alert( + const std::string& alert_name, + const backend::EntityId& domain_id, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, + const backend::AlertKind& alert_kind, + double threshold, + const std::chrono::milliseconds& t_between_triggers) +{ + try + { + StatisticsBackend::set_alert(alert_name, domain_id, host_name, user_name, topic_name, alert_kind, threshold, + t_between_triggers); + } + catch (const Exception& e) + { + qWarning() << "Fail setting new alert"; + static_cast(e); + } +} + +void SyncBackendConnection::remove_alert( + const backend::AlertId& id) +{ + try + { + StatisticsBackend::remove_alert(id); + } + catch (const Exception& e) + { + qWarning() << "Fail removing alert"; + static_cast(e); + } +} + bool SyncBackendConnection::update_host( models::ListModel* physical_model, EntityId id, @@ -1798,6 +1921,13 @@ std::vector SyncBackendConnection::get_data_kinds() }); } +std::vector SyncBackendConnection::get_alert_kinds() +{ + return std::vector({ + "NEW_DATA", + "NO_DATA"}); +} + std::vector> SyncBackendConnection::get_data_supported_entity_kinds( DataKind data_kind) { diff --git a/src/backend/backend_utils.cpp b/src/backend/backend_utils.cpp index 63e88d50..9a50dfe7 100644 --- a/src/backend/backend_utils.cpp +++ b/src/backend/backend_utils.cpp @@ -101,6 +101,35 @@ QString entity_kind_to_QString( } } +models::AlertId alert_backend_id_to_models_id( + const AlertId& id) +{ + std::ostringstream stream; + stream << id; + return utils::to_QString(stream.str()); +} + +AlertId alert_models_id_to_backend_id( + const models::AlertId& id) +{ + return AlertId(id.toInt()); +} + +QString alert_kind_to_QString( + const AlertKind& alert_kind) +{ + switch (alert_kind) + { + case AlertKind::NEW_DATA_ALERT: + return "New Data"; + case AlertKind::NO_DATA_ALERT: + return "No Data"; + case AlertKind::INVALID_ALERT: + default: + return "INVALID"; + } +} + std::string statistic_kind_to_string( const StatisticKind& statistic_kind) { @@ -294,6 +323,25 @@ StatisticKind string_to_statistic_kind( } } +AlertKind string_to_alert_kind( + const QString& alert_kind) +{ + static std::unordered_map const conversionTable = { + {"NO_DATA", AlertKind::NO_DATA_ALERT}, + {"NEW_DATA", AlertKind::NEW_DATA_ALERT}, + }; + + auto it = conversionTable.find(utils::to_string(alert_kind)); + if (it != conversionTable.end()) + { + return it->second; + } + else + { + return AlertKind::INVALID_ALERT; + } +} + std::string get_info_value( const EntityInfo& info, const std::string& key) @@ -530,6 +578,20 @@ std::string entity_status_description( } } +std::string entity_alert_description( + const backend::AlertKind kind) +{ + switch (kind){ + case backend::AlertKind::NO_DATA_ALERT: + return "No data has been received for the entity in the defined time period"; + case backend::AlertKind::NEW_DATA_ALERT: + return "New data on the entity has been received"; + default: + case backend::AlertKind::INVALID_ALERT: + return ""; + } +} + std::string policy_documentation_description( const uint32_t& id) { diff --git a/src/model/alerts/AlertListItem.cpp b/src/model/alerts/AlertListItem.cpp new file mode 100644 index 00000000..48daa2ea --- /dev/null +++ b/src/model/alerts/AlertListItem.cpp @@ -0,0 +1,126 @@ +// Copyright 2025 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 +#include + +namespace models { + +AlertListItem::AlertListItem( + QObject* parent) + : QObject(parent) + , id_(0) +{ +} + +AlertListItem::AlertListItem( + backend::AlertId id, + QObject* parent) + : QObject(parent) + , id_(id) +{ +} + +AlertListItem::AlertListItem( + backend::AlertId id, + backend::AlertSummary info, + QObject* parent) + : QObject(parent) + , id_(id) + , info_(info) + , clicked_(false) +{ +} + +AlertListItem::~AlertListItem() +{ +} + +QString AlertListItem::alert_id() const +{ + return backend::backend_id_to_models_id(id_); //backend::backend_id_to_models_id(id_); +} + +QString AlertListItem::name() const +{ + return utils::to_QString(backend::get_alias(info_)); +} + +QString AlertListItem::kind() const +{ + return backend::alert_kind_to_QString(backend_kind()); +} + +bool AlertListItem::alive() const +{ + return backend::get_info_alive(info_); +} + +bool AlertListItem::clicked() const +{ + return clicked_; +} + +backend::AlertSummary AlertListItem::info() const +{ + return info_; +} + +backend::AlertId AlertListItem::get_alert_id() const +{ + return id_; +} + +QVariant AlertListItem::data( + int role) const +{ + switch (role) + { + case idRole: + return this->alert_id(); + case nameRole: + return this->name(); + case kindRole: + return this->kind(); + case aliveRole: + return this->alive(); + case clickedRole: + return this->clicked(); + default: + return QVariant(); + } +} + +QHash AlertListItem::roleNames() const +{ + QHash roles; + + roles[idRole] = "id"; + roles[nameRole] = "name"; + roles[kindRole] = "kind"; + roles[aliveRole] = "alive"; + roles[clickedRole] = "clicked"; + + return roles; +} + +} //namespace models diff --git a/src/model/alerts/AlertListModel.cpp b/src/model/alerts/AlertListModel.cpp new file mode 100644 index 00000000..2c78e068 --- /dev/null +++ b/src/model/alerts/AlertListModel.cpp @@ -0,0 +1,277 @@ +// Copyright 2025 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 { + +AlertListModel::AlertListModel( + AlertListItem* prototype, + QObject* parent) + : QAbstractListModel(parent) +{ + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + prototype_ = prototype; + items_ = QList(); +} + +AlertListModel::~AlertListModel() +{ + delete prototype_; +} + +int AlertListModel::rowCount( + const QModelIndex&) const +{ + return items_.size(); +} + +QVariant AlertListModel::data( + const QModelIndex& index, + int role) const +{ + if (index.row() >= 0 && index.row() < items_.size()) + { + return items_.at(index.row())->data(role); + } + return QVariant(); +} + +QHash AlertListModel::roleNames() const +{ + return prototype_->roleNames(); +} + +void AlertListModel::appendRow( + AlertListItem* item) +{ + if (item != nullptr) + { + appendRows(QList() << item); + emit countChanged(rowCount()); + } +} + +void AlertListModel::appendRows( + QList& items) +{ + if (items.size() == 0) + { + return; + } + + beginInsertRows(QModelIndex(), rowCount(), rowCount() + items.size() - 1); + foreach(AlertListItem * item, items) + { + QObject::connect(item, SIGNAL(dataChanged()), this, SLOT(updateItem())); + items_.append(item); + } + endInsertRows(); + + emit countChanged(rowCount()); +} + +void AlertListModel::insertRow( + int row, + AlertListItem* item) +{ + if (item == nullptr) + { + return; + } + + beginInsertRows(QModelIndex(), row, row); + QObject::connect(item, SIGNAL(dataChanged()), this, SLOT(updateItem())); + items_.insert(row, item); + endInsertRows(); + emit countChanged(rowCount()); +} + +bool AlertListModel::removeRow( + int row, + const QModelIndex& index) +{ + if (row >= 0 && row < items_.size()) + { + beginRemoveRows(index, row, row); + AlertListItem* item = items_.takeAt(row); + delete item; + endRemoveRows(); + emit countChanged(rowCount()); + return true; + } + return false; +} + +bool AlertListModel::removeRows( + int row, + int count, + const QModelIndex& index) +{ + if (row >= 0 && count > 0 && (row + count) <= items_.size()) + { + beginRemoveRows(index, row, row + count - 1); + for (int i = 0; i < count; i++) + { + AlertListItem* item = items_.takeAt(row); + delete item; + item = nullptr; + } + endRemoveRows(); + emit countChanged(rowCount()); + return true; + } + return false; +} + +void AlertListModel::clear() +{ + if (items_.size() == 0) + { + return; + } + removeRows(0, items_.size()); + emit countChanged(rowCount()); +} + +QModelIndex AlertListModel::indexFromItem( + AlertListItem* item) const +{ + if (item != nullptr) + { + for (int i = 0; i < items_.size(); i++) + { + if (items_.at(i) == item) + { + return index(i); + } + } + } + return QModelIndex(); +} + +AlertListItem* AlertListModel::find( + AlertId itemId) const +{ + foreach(AlertListItem * item, items_) + { + if (item->alert_id() == itemId) + { + return item; + } + } + return nullptr; +} + +AlertListItem* AlertListModel::find( + backend::AlertId itemId) const +{ + foreach(AlertListItem * item, items_) + { + if (item->get_alert_id() == itemId) + { + return item; + } + } + return nullptr; +} + +AlertListItem* AlertListModel::at( + int index) const +{ + return items_.at(index); +} + +int AlertListModel::getRowFromItem( + AlertListItem* item) const +{ + if (item != nullptr) + { + for (int i = 0; i < items_.size(); i++) + { + if (items_.at(i) == item) + { + return i; + } + } + } + return -1; +} + +QList AlertListModel::to_QList() const +{ + return items_; +} + +void AlertListModel::updateItem() +{ + AlertListItem* item = static_cast(sender()); + QModelIndex index = indexFromItem(item); + if (index.isValid()) + { + emit dataChanged(index, index); + } +} + +QVariant AlertListModel::get( + int index) +{ + if (index >= items_.size() || index < 0) + { + return QVariant(); + } + AlertListItem* item = items_.at(index); + QMap itemData; + QHashIterator hashItr(item->roleNames()); + + while (hashItr.hasNext()) + { + hashItr.next(); + itemData.insert(hashItr.value(), QVariant(item->data(hashItr.key()))); + } + return QVariant(itemData); +} + +int AlertListModel::rowIndexFromId( + AlertId id) +{ + AlertListItem* item = find(id); + + if (item) + { + return indexFromItem(item).row(); + } + return -1; +} + +int AlertListModel::rowIndexFromId( + backend::AlertId id) +{ + AlertListItem* item = find(id); + + if (item) + { + return indexFromItem(item).row(); + } + return -1; +} + +} // namespace models diff --git a/src/model/tree/TreeItem.cpp b/src/model/tree/TreeItem.cpp index ad2dbe3d..b6184a89 100644 --- a/src/model/tree/TreeItem.cpp +++ b/src/model/tree/TreeItem.cpp @@ -46,6 +46,12 @@ TreeItem* TreeItem::child_item( return child_items_.value(row); } +void TreeItem::remove_child_item( + int row) +{ + child_items_.removeAt(row); +} + int TreeItem::child_count() const { return child_items_.count(); diff --git a/src/model/tree/TreeModel.cpp b/src/model/tree/TreeModel.cpp index 1a0e09e2..c84e72c2 100644 --- a/src/model/tree/TreeModel.cpp +++ b/src/model/tree/TreeModel.cpp @@ -16,6 +16,8 @@ // along with eProsima Fast DDS Monitor. If not, see . #include +#include +#include #include #include @@ -282,4 +284,158 @@ void TreeModel::update( emit updatedData(); } +TreeItem* TreeModel::find_child_by_name( + TreeItem* parent, + const QString& name) const +{ + for (int i = 0; i < parent->child_count(); ++i) + { + TreeItem* child = parent->child_item(i); + if (child->get_item_name().toString() == name) + { + return child; + } + } + return nullptr; +} + +void TreeModel::setup_model_data_without_collapse( + TreeItem* parent, + const QModelIndex& parent_index, + const json& json_data) +{ + QHash currentIndexByName; + for (int i = 0; i < parent->child_count(); ++i) + { + QString name = parent->child_item(i)->get_item_name().toString(); + currentIndexByName[name] = i; + } + + QSet newKeys; + int insertPos = parent->child_count(); + int jsonIndex = 0; + for (auto it = json_data.begin(); it != json_data.end(); ++it, ++jsonIndex) + { + QString key = QString::fromUtf8(it.key().c_str()); + newKeys.insert(key); + + TreeItem* existingChild = find_child_by_name(parent, key); + + if (existingChild) + { + // If the node exists, its content might be updated + if (it.value().is_primitive()) + { + QString newValue; + if (it.value().is_string()) + { + newValue = QString::fromUtf8(it.value().get().c_str()); + } + else if (it.value().is_number()) + { + newValue = QString::number(it.value().get()); + } + else if (it.value().is_boolean()) + { + newValue = (it.value().get() ? "true" : "false"); + } + else + { + newValue = "-"; + } + + // Update value if changed + if (existingChild->get_item_value().toString() != newValue) + { + // Replace the value directly in TreeItem + existingChild->data(TreeItem::VALUE); // ensure correct access + existingChild->clear(); // if needed to reset children + // Update internal value + QList updatedData; + updatedData << key << newValue; + *existingChild = TreeItem(updatedData, parent); + // Notify QML about the change + QModelIndex changedIndex = createIndex(existingChild->row(), 1, existingChild); + emit dataChanged(changedIndex, changedIndex); + } + } + else + { + // Recurse deeper for nested structures + QModelIndex childIndex = createIndex(existingChild->row(), 0, existingChild); + setup_model_data_without_collapse(existingChild, childIndex, it.value()); + } + } + else + { + // If it does not exist, it is inserted + beginInsertRows(parent_index, insertPos, insertPos); + + QList rowData; + rowData << key; + + if (it.value().is_primitive()) + { + if (it.value().is_string()) + { + rowData << QString::fromUtf8(it.value().get().c_str()); + } + else if (it.value().is_number()) + { + rowData << QString::number(it.value().get()); + } + else if (it.value().is_boolean()) + { + rowData << (it.value().get() ? "true" : "false"); + } + else + { + rowData << "-"; + } + } + else + { + rowData << ""; + } + + TreeItem* newChild = new TreeItem(rowData, parent); + parent->append_child(newChild); + + endInsertRows(); + + if (!it.value().is_primitive()) + { + QModelIndex newIndex = createIndex(insertPos, 0, newChild); + setup_model_data_without_collapse(newChild, newIndex, it.value()); + } + + insertPos++; + } + } + + // Remove nodes that are not present in the data + for (int i = parent->child_count() - 1; i >= 0; --i) + { + TreeItem* child = parent->child_item(i); + QString name = child->get_item_name().toString(); + + if (!newKeys.contains(name)) + { + beginRemoveRows(parent_index, i, i); + delete child; + parent->remove_child_item(i); // direct removal + endRemoveRows(); + } + } +} + +void TreeModel::update_without_collapse( + json& data) +{ + std::unique_lock lock(update_mutex_); + // Recursive function to update without collapsing + setup_model_data_without_collapse(root_item_, QModelIndex(), data); + emit updatedData(); +} + } // namespace models