From 07e13278a0baa11eb3f9ffa459c12ac9b472b54f Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 16 Sep 2025 07:56:37 +0200 Subject: [PATCH 01/42] Adding GUI features for alerts Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 3 + include/fastdds_monitor/Engine.h | 12 +- .../backend/SyncBackendConnection.h | 9 +- qml.qrc | 3 + qml/AlertKindDialog.qml | 94 ++++++++++ qml/MonitorMenuBar.qml | 4 + qml/MonitorToolBar.qml | 9 + qml/NewDataAlertDialog.qml | 135 ++++++++++++++ qml/NoDataAlertDialog.qml | 172 ++++++++++++++++++ qml/Panels.qml | 4 + qml/StatusLayout.qml | 19 ++ qml/TabLayout.qml | 4 + qml/main.qml | 26 +++ src/Controller.cpp | 5 + src/Engine.cpp | 5 + src/backend/SyncBackendConnection.cpp | 7 + 16 files changed, 505 insertions(+), 6 deletions(-) create mode 100644 qml/AlertKindDialog.qml create mode 100644 qml/NewDataAlertDialog.qml create mode 100644 qml/NoDataAlertDialog.qml diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index e85e3d60..1ad56bcb 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -274,6 +274,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..7cd9b11e 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -532,15 +532,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); @@ -821,6 +824,9 @@ public slots: //! TODO models::ListModel* destination_entity_id_model_; + //! Model to hold the data about the alerts created + models::ListModel* alert_entity_id_model_; + //! Ids of the last Entity clicked EntitiesClicked last_entities_clicked_; diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 8309e137..d1189346 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -319,15 +319,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); diff --git a/qml.qrc b/qml.qrc index 064c48a4..960cbcf6 100644 --- a/qml.qrc +++ b/qml.qrc @@ -16,6 +16,9 @@ qml/AboutDialog.qml qml/AdaptiveComboBox.qml qml/AdaptiveMenu.qml + qml/AlertKindDialog.qml + qml/NewDataAlertDialog.qml + qml/NoDataAlertDialog.qml qml/ChangeAliasDialog.qml qml/ChartsLayout.qml qml/CustomLegend.qml diff --git a/qml/AlertKindDialog.qml b/qml/AlertKindDialog.qml new file mode 100644 index 00000000..e7c8d645 --- /dev/null +++ b/qml/AlertKindDialog.qml @@ -0,0 +1,94 @@ +// 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.0 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 +import Theme 1.0 + +Dialog { + id: alertKindDialog + modal: false + title: "Create new alert" + standardButtons: Dialog.Ok | Dialog.Cancel + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + property var availableAlertKinds: [] + + signal createAlert(string alertKind) + + onAccepted: { + if (!checkInputs()) + return + createAlert(alertKindComboBox.currentText) + } + + Component.onCompleted: { + availableAlertKinds = controller.get_alert_kinds() + } + + onAboutToShow: { + alertKindComboBox.currentIndex = -1 + } + + 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 + } + + } + + MessageDialog { + id: emptyAlertKind + title: "Alert Kind" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The alert kind field is empty. Please choose an alert kind from the list." + onAccepted: alertKindDialog.open() + onDiscard: alertKindDialog.close() + } + + function checkInputs() { + if (alertKindComboBox.currentIndex === -1) { + emptyAlertKind.open() + return false + } + + return true + } + +} diff --git a/qml/MonitorMenuBar.qml b/qml/MonitorMenuBar.qml index 45d0a528..84d7febe 100644 --- a/qml/MonitorMenuBar.qml +++ b/qml/MonitorMenuBar.qml @@ -74,6 +74,10 @@ MenuBar { text: qsTr("Display Real-&Time Data") onTriggered: dynamicDataKindDialog.open() } + Action { + text: qsTr("Create Alert") + onTriggered: alertKindDialog.open() + } MenuSeparator { } Action { text: qsTr("Delete inactive entities") diff --git a/qml/MonitorToolBar.qml b/qml/MonitorToolBar.qml index a58f5a94..fb6fe492 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: true property bool isVisibleRefresh: true property bool isVisibleClearLog: false property bool isVisibleClearIssues: false @@ -76,6 +77,14 @@ ToolBar { onClicked: dynamicDataKindDialog.open() } + MonitorToolBarButton { + id: alertChart + iconName: "alerts" + tooltipText: "Create alert" + visible: isVisibleCreateAlert + onClicked: alertKindDialog.open() + } + MonitorToolBarButton { id: refresh iconName: "refresh" diff --git a/qml/NewDataAlertDialog.qml b/qml/NewDataAlertDialog.qml new file mode 100644 index 00000000..f3af0953 --- /dev/null +++ b/qml/NewDataAlertDialog.qml @@ -0,0 +1,135 @@ +// 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.0 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 +import Theme 1.0 + +Dialog { + id: newDataAlertKindDialog + modal: false + title: "Create new alert" + standardButtons: Dialog.Ok | Dialog.Cancel + + property bool activeOk: true + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + signal createAlert() + + Component.onCompleted: { + standardButton(Dialog.Ok).text = qsTrId("Add") + standardButton(Dialog.Cancel).text = qsTrId("Close") + + // Get the available topics from the backend + controller.update_available_entity_ids("Topic", "getDataDialogSourceEntityId") + } + + onAccepted: { + if (!checkInputs()) + return + + createAlert() + } + + onAboutToShow: { + getDataDialogSourceEntityId.currentIndex = 0 + controller.update_available_entity_ids("Topic", "getDataDialogSourceEntityId") + + } + + GridLayout{ + + columns: 2 + rowSpacing: 20 + + Label { + id: seriesLabel + text: "Alert label: " + InfoToolTip { + text: "Name of the alert.\n" + } + } + TextField { + id: alertTextField + placeholderText: "" + selectByMouse: true + maximumLength: 50 + Layout.fillWidth: true + + onTextEdited: activeOk = true + } + + + Label { + id: sourceEntityIdLabel + text: "Source Entity Id: " + InfoToolTip { + text: "Entity from which the data\n" + + "will be collected." + } + } + RowLayout { + AdaptiveComboBox { + id: getDataDialogSourceEntityId + model: [ + "Topic"] + + onActivated: { + activeOk = true + } + } + AdaptiveComboBox { + id: sourceEntityId + textRole: "nameId" + valueRole: "id" + displayText: currentIndex === -1 + ? ("Please choose a " + getDataDialogSourceEntityId.currentText + "...") + : currentText + model: entityModelFirst + + onActivated: { + activeOk = true + } + } + } + + } + + MessageDialog { + id: emptyTopic + title: "Topic not selected" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The topic field is empty. Please choose a topic from the list." + onAccepted: newDataAlertKindDialog.open() + onDiscard: newDataAlertKindDialog.close() + } + + function checkInputs() { + if (currentTopic.currentIndex === -1) { + emptyTopic.open() + return false + } + + return true + } + +} diff --git a/qml/NoDataAlertDialog.qml b/qml/NoDataAlertDialog.qml new file mode 100644 index 00000000..d69b6408 --- /dev/null +++ b/qml/NoDataAlertDialog.qml @@ -0,0 +1,172 @@ +// 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.0 +import QtQuick.Dialogs 1.2 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.3 +import Theme 1.0 + +Dialog { + id: noDataAlertKindDialog + modal: false + title: "Create new alert" + standardButtons: Dialog.Ok | Dialog.Cancel + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + property bool activeOk: true + + Component.onCompleted: { + standardButton(Dialog.Ok).text = qsTrId("Add") + standardButton(Dialog.Cancel).text = qsTrId("Close") + controller.update_available_entity_ids("Host", "getDataDialogSourceEntityId") + } + + onAboutToShow: { + getDataDialogSourceEntityId.currentIndex = 0 + updateAllEntities() + sourceEntityId.currentIndex = -1 + } + + onAccepted: { + if (!checkInputs()) + return + + if (activeOk) { + createAlert() + } + activeOk = true + } + + onApplied: { + if (!checkInputs()) + return + + if (activeOk) { + createAlert() + } + activeOk = false + statisticKind.currentIndex = -1 + cumulative.checked = false + } + + onClosed: activeOk = true + + GridLayout{ + + columns: 2 + rowSpacing: 20 + + Label { + id: alertLabel + text: "Alert label: " + InfoToolTip { + text: "Name of the alert.\n"+ + "The alert name is autogerated\n" + + "using the values given in this\n" + + "dialog." + } + } + TextField { + id: alertLabelTextField + placeholderText: "" + selectByMouse: true + maximumLength: 100 + Layout.fillWidth: true + + onTextEdited: activeOk = true + } + + + Label { + id: entityKindLabel + text: "Entity kind: " + InfoToolTip { + text: "Entity kind from which the data\n" + + "will be collected." + } + } + + RowLayout { + AdaptiveComboBox { + id: getDataDialogSourceEntityId + model: [ + "Host", + "User", + "Process", + "Domain", + "Topic", + "DomainParticipant", + "DataWriter", + "DataReader", + "Locator"] + + onActivated: { + activeOk = true + updateEntities() + } + } + AdaptiveComboBox { + id: sourceEntityId + textRole: "nameId" + valueRole: "id" + displayText: currentIndex === -1 + ? ("Please choose a " + getDataDialogSourceEntityId.currentText + "...") + : currentText + model: entityModelFirst + + onActivated: { + activeOk = true + } + } + } + + Label { + text: "Threshold: " + InfoToolTip { + text: "Threshold of the throughput under which the alert will start triggering." + } + } + SpinBox { + id: noDataThreshold + editable: true + from: 1 + to: 100 + stepSize: 1 + value: 5 + } + } + + MessageDialog { + id: emptyEntityIdDialog + title: "Empty Entity Id" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The Entity Id field is empty. Please choose an Entity Id from the list." + onAccepted: noDataAlertDialog.open() + onDiscard: noDataAlertDialog.close() + } + + + function updateEntities() { + controller.update_available_entity_ids(getDataDialogSourceEntityId.currentText, "getDataDialogSourceEntityId") + sourceEntityId.recalculateWidth() + regenerateSeriesLabel() + } +} diff --git a/qml/Panels.qml b/qml/Panels.qml index cf8c0e6a..c641dfa0 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -175,6 +175,10 @@ RowLayout { tabs.chartsLayout_createDynamicChart(dataKind, timeWindowSeconds, updatePeriod, maxPoints) } + function createAlert(alertKind){ + tabs.createAlert(alertKind) + } + function createScheduleClear(entities, data, updateData, updateClear){ tabs.chartsLayout_createScheduleClear(entities, data, updateData, updateClear) } diff --git a/qml/StatusLayout.qml b/qml/StatusLayout.qml index beca6d9f..5f828a7a 100644 --- a/qml/StatusLayout.qml +++ b/qml/StatusLayout.qml @@ -99,6 +99,25 @@ Item } } + // Main Alerts tab + Tab { + title: "Alerts" + Rectangle { + + color: "white" + + // Main content of alerts tab: alert tree view with alerts per entity + StatusTreeView { + id: status_tree_view + anchors.fill: parent + anchors.margins: 1 + + model: entityStatusModel // problems model: entity status proxy model + } + } + } + + // Tab main stlye style: TabViewStyle { frameOverlap: 1 diff --git a/qml/TabLayout.qml b/qml/TabLayout.qml index f2fc9818..afa5ca78 100644 --- a/qml/TabLayout.qml +++ b/qml/TabLayout.qml @@ -968,6 +968,10 @@ Item { chartsLayout.createDynamicChart(dataKind, timeWindowSeconds, updatePeriod, maxPoints) } + function createAlert(dataKind){ + console.log("Creating alert of kind: " + dataKind) + } + function chartsLayout_createScheduleClear(entities, data, updateData, updateClear){ chartsLayout.createScheduleClear(entities, data, updateData, updateClear) } diff --git a/qml/main.qml b/qml/main.qml index 80527888..35ebf2aa 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -119,6 +119,32 @@ ApplicationWindow { onCreateChart: panels.createDynamicChart(dataKind, timeWindowSeconds, updatePeriod, maxPoints) } + AlertKindDialog { + id: alertKindDialog + onCreateAlert: { + if (alertKind === "NEW_DATA_ON_TOPIC") newDataAlertDialog.open() + else if (alertKind === "NO_DATA_ON_TOPIC") noDataAlertDialog.open() + } + } + + NewDataAlertDialog { + id: newDataAlertDialog + // onCreateAlert: { + // if (alertKind === "NEW_DATA_ON_TOPIC") newDataAlertDialog.open() + // else if (alertKind === "NO_DATA_ON_TOPIC") noDataAlertDialog.open() + // } + } + + + NoDataAlertDialog { + id: noDataAlertDialog + // onCreateAlert: { + // if (alertKind === "NEW_DATA_ON_TOPIC") newDataAlertDialog.open() + // else if (alertKind === "NO_DATA_ON_TOPIC") noDataAlertDialog.open() + // } + } + + ScheduleClearDialog { id: scheduleClear } diff --git a/src/Controller.cpp b/src/Controller.cpp index bb90072b..c204a195 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -342,6 +342,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..8b29bfbc 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -1847,6 +1847,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/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 96576899..90abccbf 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -1798,6 +1798,13 @@ std::vector SyncBackendConnection::get_data_kinds() }); } +std::vector SyncBackendConnection::get_alert_kinds() +{ + return std::vector({ + "NEW_DATA_ON_TOPIC", + "NO_DATA_ON_TOPIC"}); +} + std::vector> SyncBackendConnection::get_data_supported_entity_kinds( DataKind data_kind) { From 15c216fc5952782e5f5435bd5886da59b92899dc Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 16 Sep 2025 09:51:15 +0200 Subject: [PATCH 02/42] Refs #23615 #23616, more changes to GUI and icons Signed-off-by: Emilio Cuesta --- qml.qrc | 4 ++ qml/MonitorToolBar.qml | 2 +- qml/NewDataAlertDialog.qml | 11 ++- qml/NoDataAlertDialog.qml | 13 ++-- resources/images/icons/alert/alert_black.svg | 72 +++++++++++++++++++ .../icons/alert/alert_eProsimaLightBlue.svg | 72 +++++++++++++++++++ resources/images/icons/alert/alert_grey.svg | 72 +++++++++++++++++++ resources/images/icons/alert/alert_white.svg | 72 +++++++++++++++++++ 8 files changed, 309 insertions(+), 9 deletions(-) create mode 100644 resources/images/icons/alert/alert_black.svg create mode 100644 resources/images/icons/alert/alert_eProsimaLightBlue.svg create mode 100644 resources/images/icons/alert/alert_grey.svg create mode 100644 resources/images/icons/alert/alert_white.svg diff --git a/qml.qrc b/qml.qrc index 960cbcf6..62f88115 100644 --- a/qml.qrc +++ b/qml.qrc @@ -84,6 +84,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/MonitorToolBar.qml b/qml/MonitorToolBar.qml index fb6fe492..6654149a 100644 --- a/qml/MonitorToolBar.qml +++ b/qml/MonitorToolBar.qml @@ -79,7 +79,7 @@ ToolBar { MonitorToolBarButton { id: alertChart - iconName: "alerts" + iconName: "alert" tooltipText: "Create alert" visible: isVisibleCreateAlert onClicked: alertKindDialog.open() diff --git a/qml/NewDataAlertDialog.qml b/qml/NewDataAlertDialog.qml index f3af0953..3063d1dc 100644 --- a/qml/NewDataAlertDialog.qml +++ b/qml/NewDataAlertDialog.qml @@ -51,8 +51,8 @@ Dialog { onAboutToShow: { getDataDialogSourceEntityId.currentIndex = 0 + alertTextField.text = "" controller.update_available_entity_ids("Topic", "getDataDialogSourceEntityId") - } GridLayout{ @@ -97,7 +97,7 @@ Dialog { } } AdaptiveComboBox { - id: sourceEntityId + id: currentTopic textRole: "nameId" valueRole: "id" displayText: currentIndex === -1 @@ -124,7 +124,7 @@ Dialog { } function checkInputs() { - if (currentTopic.currentIndex === -1) { + if (currentTopic.currentIndex === -1 || alertTextField.text === "") { emptyTopic.open() return false } @@ -132,4 +132,9 @@ Dialog { return true } + function updateEntities() { + controller.update_available_entity_ids(getDataDialogSourceEntityId.currentText, "getDataDialogSourceEntityId") + regenerateSeriesLabel() + } + } diff --git a/qml/NoDataAlertDialog.qml b/qml/NoDataAlertDialog.qml index d69b6408..aef115e8 100644 --- a/qml/NoDataAlertDialog.qml +++ b/qml/NoDataAlertDialog.qml @@ -42,6 +42,7 @@ Dialog { getDataDialogSourceEntityId.currentIndex = 0 updateAllEntities() sourceEntityId.currentIndex = -1 + alertTextField.text = "" } onAccepted: { @@ -84,7 +85,7 @@ Dialog { } } TextField { - id: alertLabelTextField + id: alertTextField placeholderText: "" selectByMouse: true maximumLength: 100 @@ -163,10 +164,12 @@ Dialog { onDiscard: noDataAlertDialog.close() } + function checkInputs() { + if (currentTopic.currentIndex === -1 || alertTextField.text === "") { + emptyEntityIdDialog.open() + return false + } - function updateEntities() { - controller.update_available_entity_ids(getDataDialogSourceEntityId.currentText, "getDataDialogSourceEntityId") - sourceEntityId.recalculateWidth() - regenerateSeriesLabel() + return true } } 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cbbd3333fc3b1d6f8ca1c941fbc9ee0e67a1a323 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 16 Sep 2025 15:04:29 +0200 Subject: [PATCH 03/42] Add more GUI components and prepare callbacks to bind with backend Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Engine.h | 50 +++++++ .../fastdds_monitor/backend/AlertCallback.h | 61 ++++++++ qml.qrc | 1 + qml/AlertsPanel.qml | 137 ++++++++++++++++++ qml/IconsVBar.qml | 7 +- qml/LeftPanel.qml | 9 +- qml/MonitorMenuBar.qml | 11 ++ qml/MonitorToolBar.qml | 4 +- qml/NewDataAlertDialog.qml | 26 +++- qml/NoDataAlertDialog.qml | 20 ++- qml/Panels.qml | 13 +- qml/StatusLayout.qml | 20 ++- qml/TabLayout.qml | 4 - qml/main.qml | 15 +- src/Engine.cpp | 65 +++++++++ 15 files changed, 412 insertions(+), 31 deletions(-) create mode 100644 include/fastdds_monitor/backend/AlertCallback.h create mode 100644 qml/AlertsPanel.qml diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 7cd9b11e..044d9294 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -384,6 +385,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 +448,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(); @@ -608,6 +630,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: /** @@ -622,6 +650,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: /** @@ -743,12 +777,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); @@ -757,6 +797,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, @@ -851,12 +895,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..2faae54e --- /dev/null +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -0,0 +1,61 @@ +// Copyright 2023 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// This file is part of eProsima Fast DDS Monitor. +// +// eProsima Fast DDS Monitor is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// eProsima Fast DDS Monitor is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with eProsima Fast DDS Monitor. If not, see . + +/** + * @file AlertCallback.h + */ + +#ifndef _EPROSIMA_FASTDDS_MONITOR_BACKEND_ALERT_CALLBACK_H +#define _EPROSIMA_FASTDDS_MONITOR_BACKEND_ALERT_CALLBACK_H + +#include + +namespace backend { + +/* + * Struct that store the alert callback information required by the GUI. + * It encapsulates the domain id, entity id and the kind of the new alert reported. + */ +struct AlertCallback +{ + //! Void constructor to use copy constructor afterwards + AlertCallback() + { + } + + //! Standard constructor with the two fields required + AlertCallback( + backend::EntityId domain_entity_id, + backend::EntityId entity_id, + backend::StatusKind status_kind) + : domain_entity_id(domain_entity_id) + , entity_id(entity_id) + , status_kind(status_kind) + { + } + + //! Information of the domain \c EntityId the callback refers + backend::EntityId domain_entity_id; + //! Information of the \c EntityId the callback refers + backend::EntityId entity_id; + //! Information of the \c StatusKind the callback refers + backend::StatusKind status_kind; +}; + +} // namespace backend + +#endif // _EPROSIMA_FASTDDS_MONITOR_BACKEND_STATUS_CALLBACK_H diff --git a/qml.qrc b/qml.qrc index 62f88115..06d06a1c 100644 --- a/qml.qrc +++ b/qml.qrc @@ -17,6 +17,7 @@ qml/AdaptiveComboBox.qml qml/AdaptiveMenu.qml qml/AlertKindDialog.qml + qml/AlertsPanel.qml qml/NewDataAlertDialog.qml qml/NoDataAlertDialog.qml qml/ChangeAliasDialog.qml diff --git a/qml/AlertsPanel.qml b/qml/AlertsPanel.qml new file mode 100644 index 00000000..c8dbf6d9 --- /dev/null +++ b/qml/AlertsPanel.qml @@ -0,0 +1,137 @@ +// Copyright 2021 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 Status and the Log views. + */ +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: "three_dots_menu" + Layout.alignment: Qt.AlignRight + scalingFactor: 2 + color: "white" + + + MouseArea { + anchors.fill: parent + + onClicked: { + contextMenu.y = parent.y + parent.height; + contextMenu.open() + } + } + + Menu { + id: contextMenu + + Action { + id: contextMenuDDSEntities + text: "Create Alert" + checkable: false + onTriggered: { + alertKindDialog.open() + } + } + delegate: MenuItem { + id: menuItem + implicitWidth: 150 + implicitHeight: 30 + + indicator: Item { + implicitWidth: 30 + implicitHeight: 30 + Rectangle { + width: 16 + height: 16 + anchors.centerIn: parent + visible: menuItem.checkable + border.color: menuItem.highlighted ? Theme.eProsimaLightBlue : + !menuItem.checked ? Theme.grey : "black" + radius: 3 + Rectangle { + width: 10 + height: 10 + anchors.centerIn: parent + visible: menuItem.checked + color: Theme.eProsimaLightBlue + radius: 2 + } + } + } + + contentItem: Text { + leftPadding: 15 + text: menuItem.text + opacity: enabled ? 1.0 : 0.3 + color: menuItem.highlighted ? Theme.eProsimaLightBlue : + !menuItem.checked ? Theme.grey : "black" + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + } + } + } + } + + // Rectangle { + // Layout.fillHeight: true + // Layout.fillWidth: true + + // ColumnLayout { + // id: entityListLayout + // SplitView.preferredHeight: parent.height / 4 + // spacing: 10 + // visible: true + // clip: true + + // EntityList { + // id: entityList + // Layout.fillWidth: true + // Layout.alignment: Qt.AlignTop | Qt.AlignLeft + // Layout.bottomMargin: 1 + // } + // } + // } +} 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..26893a73 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] @@ -67,6 +68,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 } diff --git a/qml/MonitorMenuBar.qml b/qml/MonitorMenuBar.qml index 84d7febe..0b270f91 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 @@ -355,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 6654149a..34015000 100644 --- a/qml/MonitorToolBar.qml +++ b/qml/MonitorToolBar.qml @@ -27,7 +27,7 @@ ToolBar { property bool isVisible: false property bool isVisibleDispData: false property bool isVisibleDispDynData: true - property bool isVisibleCreateAlert: true + property bool isVisibleCreateAlert: false property bool isVisibleRefresh: true property bool isVisibleClearLog: false property bool isVisibleClearIssues: false @@ -78,7 +78,7 @@ ToolBar { } MonitorToolBarButton { - id: alertChart + id: createAlert iconName: "alert" tooltipText: "Create alert" visible: isVisibleCreateAlert diff --git a/qml/NewDataAlertDialog.qml b/qml/NewDataAlertDialog.qml index 3063d1dc..e3d0ac2e 100644 --- a/qml/NewDataAlertDialog.qml +++ b/qml/NewDataAlertDialog.qml @@ -32,7 +32,7 @@ Dialog { x: (parent.width - width) / 2 y: (parent.height - height) / 2 - signal createAlert() + signal createAlert(string topicId) Component.onCompleted: { standardButton(Dialog.Ok).text = qsTrId("Add") @@ -46,7 +46,7 @@ Dialog { if (!checkInputs()) return - createAlert() + createAlert(currentTopic.currentText) } onAboutToShow: { @@ -89,8 +89,7 @@ Dialog { RowLayout { AdaptiveComboBox { id: getDataDialogSourceEntityId - model: [ - "Topic"] + model: ["Topic"] onActivated: { activeOk = true @@ -113,6 +112,16 @@ Dialog { } + MessageDialog { + id: emptyAlertLabel + title: "Missing alert label" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The alert label field is empty. Please enter an alert label." + onAccepted: newDataAlertKindDialog.open() + onDiscard: newDataAlertKindDialog.close() + } + MessageDialog { id: emptyTopic title: "Topic not selected" @@ -124,17 +133,20 @@ Dialog { } function checkInputs() { - if (currentTopic.currentIndex === -1 || alertTextField.text === "") { + if (currentTopic.currentIndex === -1) { emptyTopic.open() return false } + if (alertTextField.text === "") { + emptyAlertLabel.open() + return false + } return true } function updateEntities() { - controller.update_available_entity_ids(getDataDialogSourceEntityId.currentText, "getDataDialogSourceEntityId") - regenerateSeriesLabel() + controller.update_available_entity_ids("Topic", "getDataDialogSourceEntityId") } } diff --git a/qml/NoDataAlertDialog.qml b/qml/NoDataAlertDialog.qml index aef115e8..87b67754 100644 --- a/qml/NoDataAlertDialog.qml +++ b/qml/NoDataAlertDialog.qml @@ -32,6 +32,8 @@ Dialog { property bool activeOk: true + signal createAlert(string entityKind, string entityId, int noDataThreshold) + Component.onCompleted: { standardButton(Dialog.Ok).text = qsTrId("Add") standardButton(Dialog.Cancel).text = qsTrId("Close") @@ -50,7 +52,7 @@ Dialog { return if (activeOk) { - createAlert() + createAlert(getDataDialogSourceEntityId.currentText, sourceEntityId.currentText, noDataThreshold.value) } activeOk = true } @@ -154,6 +156,16 @@ Dialog { } } + MessageDialog { + id: emptyAlertLabel + title: "Missing alert label" + icon: StandardIcon.Warning + standardButtons: StandardButton.Retry | StandardButton.Discard + text: "The alert label field is empty. Please enter an alert label." + onAccepted: newDataAlertKindDialog.open() + onDiscard: newDataAlertKindDialog.close() + } + MessageDialog { id: emptyEntityIdDialog title: "Empty Entity Id" @@ -165,10 +177,14 @@ Dialog { } function checkInputs() { - if (currentTopic.currentIndex === -1 || alertTextField.text === "") { + if (currentTopic.currentIndex === -1) { emptyEntityIdDialog.open() return false } + if (alertTextField.text === "") { + emptyAlertLabel.open() + return false + } return true } diff --git a/qml/Panels.qml b/qml/Panels.qml index c641dfa0..2873dd92 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -175,10 +175,6 @@ RowLayout { tabs.chartsLayout_createDynamicChart(dataKind, timeWindowSeconds, updatePeriod, maxPoints) } - function createAlert(alertKind){ - tabs.createAlert(alertKind) - } - function createScheduleClear(entities, data, updateData, updateClear){ tabs.chartsLayout_createScheduleClear(entities, data, updateData, updateClear) } @@ -210,4 +206,13 @@ RowLayout { function openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) { leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } + + function createNewDataAlert(topicId){ + leftPanel.createNewDataAlert(topicId) + } + + function createNoDataAlert(entityKind, entityId, noDataThreshold){ + leftPanel.createNoDataAlert(entityKind, entityId, noDataThreshold) + } + } diff --git a/qml/StatusLayout.qml b/qml/StatusLayout.qml index 5f828a7a..cb0454b3 100644 --- a/qml/StatusLayout.qml +++ b/qml/StatusLayout.qml @@ -108,11 +108,29 @@ Item // Main content of alerts tab: alert tree view with alerts per entity StatusTreeView { - id: status_tree_view + id: alerts_tree_view anchors.fill: parent anchors.margins: 1 model: entityStatusModel // problems model: entity status proxy model + + // display if hidden when alerts filtered (from right-click dialog) + onEntity_status_filtered:{ + collapse_alerts_layout() + } + + // filter and clean filter signal-slots management + Connections { + target: alertsLayout + + function onClean_filter_() { + alerts_tree_view.clean_filter() + } + + function onFocus_entity_(entityId) { + alerts_tree_view.filter_model_by_id(entityId) + } + } } } } diff --git a/qml/TabLayout.qml b/qml/TabLayout.qml index afa5ca78..f2fc9818 100644 --- a/qml/TabLayout.qml +++ b/qml/TabLayout.qml @@ -968,10 +968,6 @@ Item { chartsLayout.createDynamicChart(dataKind, timeWindowSeconds, updatePeriod, maxPoints) } - function createAlert(dataKind){ - console.log("Creating alert of kind: " + dataKind) - } - function chartsLayout_createScheduleClear(entities, data, updateData, updateClear){ chartsLayout.createScheduleClear(entities, data, updateData, updateClear) } diff --git a/qml/main.qml b/qml/main.qml index 35ebf2aa..03c742e5 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 @@ -129,19 +130,17 @@ ApplicationWindow { NewDataAlertDialog { id: newDataAlertDialog - // onCreateAlert: { - // if (alertKind === "NEW_DATA_ON_TOPIC") newDataAlertDialog.open() - // else if (alertKind === "NO_DATA_ON_TOPIC") noDataAlertDialog.open() - // } + onCreateAlert: { + panels.createNewDataAlert(topicId) + } } NoDataAlertDialog { id: noDataAlertDialog - // onCreateAlert: { - // if (alertKind === "NEW_DATA_ON_TOPIC") newDataAlertDialog.open() - // else if (alertKind === "NO_DATA_ON_TOPIC") noDataAlertDialog.open() - // } + onCreateAlert: { + panels.createNoDataAlert(entityKind, entityId, noDataThreshold) + } } diff --git a/src/Engine.cpp b/src/Engine.cpp index 8b29bfbc..bedbd5f4 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -159,6 +159,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; @@ -935,6 +941,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 +962,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 +992,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 +1014,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 +1049,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 +1108,18 @@ 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_); + + // Add callback to log model + add_log_callback_("New alert reported!", utils::now()); + + return true; +} + bool Engine::update_entity_status( const backend::EntityId& id, backend::StatusKind kind) From 65566fb2379ea8c7ee19b7897ec9bfbaa2eff48a Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 16 Sep 2025 16:46:19 +0200 Subject: [PATCH 04/42] Alert request reaches controller Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 4 + qml.qrc | 1 + qml/AlertList.qml | 280 +++++++++++++++++++++++++++ qml/AlertsPanel.qml | 39 ++-- qml/LeftPanel.qml | 11 ++ qml/NewDataAlertDialog.qml | 18 +- qml/NoDataAlertDialog.qml | 6 +- qml/TopicMenu.qml | 8 + src/Controller.cpp | 5 + 9 files changed, 342 insertions(+), 30 deletions(-) create mode 100644 qml/AlertList.qml diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 1ad56bcb..30c5f3ea 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -229,6 +229,10 @@ public slots: QString new_alias, QString entity_kind); + //! Sets an alert + void set_alert( + QString entity_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); diff --git a/qml.qrc b/qml.qrc index 06d06a1c..a10c1ce0 100644 --- a/qml.qrc +++ b/qml.qrc @@ -17,6 +17,7 @@ qml/AdaptiveComboBox.qml qml/AdaptiveMenu.qml qml/AlertKindDialog.qml + qml/AlertList.qml qml/AlertsPanel.qml qml/NewDataAlertDialog.qml qml/NoDataAlertDialog.qml diff --git a/qml/AlertList.qml b/qml/AlertList.qml new file mode 100644 index 00000000..25d52c28 --- /dev/null +++ b/qml/AlertList.qml @@ -0,0 +1,280 @@ +// Copyright 2021 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: alertList + Layout.fillHeight: true + Layout.fillWidth: true + + enum DDSEntity { + Participant, + Endpoint, + Locator + } + + 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: participantList + model: participantModel + delegate: participantListDelegate + clip: true + width: parent.width + height: parent.height + spacing: verticalSpacing + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.vertical: CustomScrollBar { + id: scrollBar + } + } + + Component { + id: participantListDelegate + + Item { + id: participantItem + width: participantList.width + height: participantListColumn.childrenRect.height + + property var participantId: id + property int participantIdx: index + property var endpointList: endpointList + + Column { + id: participantListColumn + + Rectangle { + id: participantHighlightRect + width: alertList.width + height: participantIcon.height + color: highligthRow(clicked) + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onDoubleClicked: { + if(endpointList.height === endpointList.collapseHeightFlag) { + endpointList.height = 0; + } else { + if (endpointList.childrenRect.height != 0) { + endpointList.height = endpointList.collapseHeightFlag; + } + } + } + onClicked: { + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(controller.get_domain_id(id), id, name, kind, openMenuCaller.leftPanel) + } else { + controller.participant_click(id) + } + } + } + + RowLayout { + spacing: spacingIconLabel + + IconSVG { + id: participantIcon + name: "participant" + size: iconSize + Layout.leftMargin: firstIndentation + color: entityLabelColor(clicked, alive) + } + Label { + text: name + color: entityLabelColor(clicked, alive) + } + } + } + + ListView { + id: endpointList + model: participantModel.subModelFromEntityId(participantId) + width: participantList.width + height: 0 + contentHeight: contentItem.childrenRect.height + clip: true + spacing: verticalSpacing + topMargin: verticalSpacing + delegate: endpointListDelegate + boundsBehavior: Flickable.StopAtBounds + + property int collapseHeightFlag: childrenRect.height + endpointList.topMargin + } + + Component { + id: endpointListDelegate + + Item { + id: endpointItem + height: endpointListColumn.childrenRect.height + + property var endpointId: id + property int endpointIdx: index + property var locatorList: locatorList + + ListView.onAdd: { + if(endpointList.height != 0) { + endpointList.height = endpointList.collapseHeightFlag; + } + } + + Column { + id: endpointListColumn + + Rectangle { + id: endpointHighlightRect + width: alertList.width + height: endpointIcon.height + color: highligthRow(clicked) + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onDoubleClicked: { + if(locatorList.height === locatorList.collapseHeightFlag) { + locatorList.height = 0; + endpointList.height = + endpointList.height - locatorList.collapseHeightFlag; + } else { + if (locatorList.childrenRect.height != 0) { + locatorList.height = locatorList.collapseHeightFlag; + endpointList.height = endpointList.height + locatorList.height; + } + } + } + onClicked: { + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(controller.get_domain_id(id), id, name, kind, openMenuCaller.leftPanel) + } else { + controller.endpoint_click(id) + } + } + } + + RowLayout { + spacing: spacingIconLabel + + IconSVG { + id: endpointIcon + name: (kind == "DataReader") ? "datareader" : "datawriter" + size: iconSize + Layout.leftMargin: secondIndentation + color: entityLabelColor(clicked, alive) + } + Label { + text: name + color: entityLabelColor(clicked, alive) + } + } + } + + ListView { + id: locatorList + model: endpointList.model.subModelFromEntityId(endpointId) + width: participantList.width + height: 0 + contentHeight: contentItem.childrenRect.height + clip: true + delegate: locatorListDelegate + spacing: verticalSpacing + topMargin: verticalSpacing + boundsBehavior: Flickable.StopAtBounds + + property int collapseHeightFlag: childrenRect.height + locatorList.topMargin + } + + Component { + id: locatorListDelegate + + Item { + id: locatorItem + width: parent.width + height: locatorListColumn.childrenRect.height + + property int locatorIdx: index + + ListView.onAdd: { + if(locatorList.height != 0) { + var prevHeight = locatorList.height + locatorList.height = locatorList.collapseHeightFlag + endpointList.height = endpointList.height + locatorList.height - prevHeight + } + } + + Column { + id: locatorListColumn + + Rectangle { + id: locatorHighlightRect + width: alertList.width + height: locatorIcon.height + color: highligthRow(clicked) + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: { + if(mouse.button & Qt.RightButton) { + openEntitiesMenu(controller.get_domain_id(id), id, name, kind, openMenuCaller.leftPanel) + } else { + controller.locator_click(id) + } + } + } + + RowLayout { + spacing: spacingIconLabel + + IconSVG { + id: locatorIcon + name: "locator" + size: iconSize + Layout.leftMargin: thirdIndentation + color: entityLabelColor(clicked, alive) + } + Label { + text: name + color: entityLabelColor(clicked, alive) + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/qml/AlertsPanel.qml b/qml/AlertsPanel.qml index c8dbf6d9..3a2dc10c 100644 --- a/qml/AlertsPanel.qml +++ b/qml/AlertsPanel.qml @@ -63,10 +63,11 @@ ColumnLayout { Menu { id: contextMenu + height: 30 Action { id: contextMenuDDSEntities - text: "Create Alert" + text: "Add Alert" checkable: false onTriggered: { alertKindDialog.open() @@ -75,7 +76,7 @@ ColumnLayout { delegate: MenuItem { id: menuItem implicitWidth: 150 - implicitHeight: 30 + implicitHeight: contextMenu.height indicator: Item { implicitWidth: 30 @@ -115,23 +116,23 @@ ColumnLayout { } } - // Rectangle { - // Layout.fillHeight: true - // Layout.fillWidth: true + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true - // ColumnLayout { - // id: entityListLayout - // SplitView.preferredHeight: parent.height / 4 - // spacing: 10 - // visible: true - // clip: true + ColumnLayout { + id: alertListLayout + SplitView.preferredHeight: parent.height / 4 + spacing: 10 + visible: true + clip: true - // EntityList { - // id: entityList - // Layout.fillWidth: true - // Layout.alignment: Qt.AlignTop | Qt.AlignLeft - // Layout.bottomMargin: 1 - // } - // } - // } + AlertList { + id: alertList + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.bottomMargin: 1 + } + } + } } diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index 26893a73..8ca9151e 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -195,4 +195,15 @@ RowLayout { function changeExplorerEntityInfo(status) { monitoringPanel.changeExplorerEntityInfo(status) } + + function createNoDataAlert(entityKind, entityId, noDataThreshold) { + console.log("MOCK: Creating no data alert for topic " + topicId) + // panels.createNoDataAlert(entityKind, entityId, noDataThreshold) + // controller.participant_click(entityId) + } + + function createNewDataAlert(topicId) { + console.log("MOCK: Creating new data alert for topic " + topicId) + controller.set_alert(topicId) + } } diff --git a/qml/NewDataAlertDialog.qml b/qml/NewDataAlertDialog.qml index e3d0ac2e..52b9b34c 100644 --- a/qml/NewDataAlertDialog.qml +++ b/qml/NewDataAlertDialog.qml @@ -22,12 +22,13 @@ import QtQuick.Layouts 1.3 import Theme 1.0 Dialog { - id: newDataAlertKindDialog + id: newDataAlertDialog modal: false title: "Create new alert" standardButtons: Dialog.Ok | Dialog.Cancel property bool activeOk: true + property string currentTopic: "" x: (parent.width - width) / 2 y: (parent.height - height) / 2 @@ -46,7 +47,8 @@ Dialog { if (!checkInputs()) return - createAlert(currentTopic.currentText) + currentTopic = topicComboBox.currentText + createAlert(currentTopic) } onAboutToShow: { @@ -96,7 +98,7 @@ Dialog { } } AdaptiveComboBox { - id: currentTopic + id: topicComboBox textRole: "nameId" valueRole: "id" displayText: currentIndex === -1 @@ -118,8 +120,8 @@ Dialog { icon: StandardIcon.Warning standardButtons: StandardButton.Retry | StandardButton.Discard text: "The alert label field is empty. Please enter an alert label." - onAccepted: newDataAlertKindDialog.open() - onDiscard: newDataAlertKindDialog.close() + onAccepted: newDataAlertDialog.open() + onDiscard: newDataAlertDialog.close() } MessageDialog { @@ -128,12 +130,12 @@ Dialog { icon: StandardIcon.Warning standardButtons: StandardButton.Retry | StandardButton.Discard text: "The topic field is empty. Please choose a topic from the list." - onAccepted: newDataAlertKindDialog.open() - onDiscard: newDataAlertKindDialog.close() + onAccepted: newDataAlertDialog.open() + onDiscard: newDataAlertDialog.close() } function checkInputs() { - if (currentTopic.currentIndex === -1) { + if (topicComboBox.currentIndex === -1) { emptyTopic.open() return false } diff --git a/qml/NoDataAlertDialog.qml b/qml/NoDataAlertDialog.qml index 87b67754..af970b25 100644 --- a/qml/NoDataAlertDialog.qml +++ b/qml/NoDataAlertDialog.qml @@ -22,7 +22,7 @@ import QtQuick.Layouts 1.3 import Theme 1.0 Dialog { - id: noDataAlertKindDialog + id: noDataAlertDialog modal: false title: "Create new alert" standardButtons: Dialog.Ok | Dialog.Cancel @@ -162,8 +162,8 @@ Dialog { icon: StandardIcon.Warning standardButtons: StandardButton.Retry | StandardButton.Discard text: "The alert label field is empty. Please enter an alert label." - onAccepted: newDataAlertKindDialog.open() - onDiscard: newDataAlertKindDialog.close() + onAccepted: noDataAlertDialog.open() + onDiscard: noDataAlertDialog.close() } MessageDialog { diff --git a/qml/TopicMenu.qml b/qml/TopicMenu.qml index f2a320e9..8a1f23cc 100644 --- a/qml/TopicMenu.qml +++ b/qml/TopicMenu.qml @@ -47,4 +47,12 @@ Menu { text: "Data type IDL view" onTriggered: openIDLView(menu.entityId) } + MenuItem { + text: "Set New Data Alarm" + onTriggered: { + newDataAlertDialog.currentTopic = menu.entityId + newDataAlertDialog.open() + } + } } + diff --git a/src/Controller.cpp b/src/Controller.cpp index c204a195..58ad4bf1 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -295,6 +295,11 @@ void Controller::set_alias( backend::string_to_entity_kind(entity_kind)); } +void Controller::set_alert(QString entity_id){ + printf("Setting alert for entity %s from the controller\n", entity_id.toStdString().c_str()); +} + + QString Controller::get_data_kind_units( QString data_kind) { From cd01ccb1920d1ec61be9390bc31b1521531da168 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 17 Sep 2025 14:41:28 +0200 Subject: [PATCH 05/42] More advances, probably not compiling yet Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 10 +++++++--- qml/LeftPanel.qml | 6 ++++-- src/Controller.cpp | 10 +++++++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 30c5f3ea..22b96932 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -229,9 +229,13 @@ public slots: QString new_alias, QString entity_kind); - //! Sets an alert - void set_alert( - QString entity_id); + //! Sets a no data alert + void set_no_data_alert( + QString entity_id, double threshold); + + //! Sets a new data alert + void set_new_data_alert( + QString topic_id); //! Give a string with the name of the unit magnitud in which each DataKind is measured QString get_data_kind_units( diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index 8ca9151e..c39eb5f5 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -197,13 +197,15 @@ RowLayout { } function createNoDataAlert(entityKind, entityId, noDataThreshold) { - console.log("MOCK: Creating no data alert for topic " + topicId) + // TODO: Remove + console.log("MOCK: Creating no data alert for topic " + topicId) // panels.createNoDataAlert(entityKind, entityId, noDataThreshold) // controller.participant_click(entityId) } function createNewDataAlert(topicId) { + // TODO: Remove console.log("MOCK: Creating new data alert for topic " + topicId) - controller.set_alert(topicId) + controller.set_new_data_alert(topicId) } } diff --git a/src/Controller.cpp b/src/Controller.cpp index 58ad4bf1..cef4ac6b 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -295,9 +295,13 @@ void Controller::set_alias( backend::string_to_entity_kind(entity_kind)); } -void Controller::set_alert(QString entity_id){ - printf("Setting alert for entity %s from the controller\n", entity_id.toStdString().c_str()); -} + //! Sets a no data alert + void set_no_data_alert( + QString entity_id, double threshold); + + //! Sets a new data alert + void set_new_data_alert( + QString topic_id); QString Controller::get_data_kind_units( From 20ae03a21caaf7a71193d3ed53d5dcadaa75d208 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Thu, 18 Sep 2025 07:33:54 +0200 Subject: [PATCH 06/42] Advancing towards alerts Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 11 +- include/fastdds_monitor/Engine.h | 14 ++ .../backend/SyncBackendConnection.h | 4 + .../fastdds_monitor/backend/backend_types.h | 1 + .../fastdds_monitor/backend/backend_utils.h | 4 + qml/main.qml | 4 +- src/Controller.cpp | 17 +-- src/Engine.cpp | 131 ++++++++++++++++++ src/backend/SyncBackendConnection.cpp | 4 +- src/backend/backend_utils.cpp | 33 +++++ 10 files changed, 205 insertions(+), 18 deletions(-) diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 22b96932..994d5031 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -230,12 +230,11 @@ public slots: QString entity_kind); //! Sets a no data alert - void set_no_data_alert( - QString entity_id, double threshold); - - //! Sets a new data alert - void set_new_data_alert( - QString topic_id); + void set_alert( + QString alert_name, + QString entity_id, + QString alert_type, + double threshold); //! Give a string with the name of the unit magnitud in which each DataKind is measured QString get_data_kind_units( diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 044d9294..3caf0a06 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -282,6 +282,17 @@ class Engine : public QQmlApplicationEngine const backend::EntityId& id, backend::StatusKind kind); + /** + * @brief Update the alert model with the status kind received + * + * @param id entity id + * @param kind AlertKind reported + * @return true if any change in model has been done + */ + bool update_entity_alert( + const backend::EntityId& id, + backend::AlertKind kind); + /** * @brief Update the entity status counters and populate the model with empty message if empty * @@ -862,6 +873,9 @@ public slots: //! Display and allow to filter Model for Fast DDS Monitor status view. models::StatusTreeModel* entity_status_proxy_model_; + //! Data Model for Fast DDS Monitor alert view. Collects all alerts detected by the monitor service + models::StatusTreeModel* entity_alert_model_; + //! TODO models::ListModel* source_entity_id_model_; diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index d1189346..636a0249 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -225,6 +225,10 @@ class SyncBackendConnection EntityId source_entity_id, ExtendedIncompatibleQosSample& sample); + bool get_alert_data( + EntityId source_entity_id, + Alert& sample); + //! Convert a given entity guid to string format std::string get_deserialized_guid( const backend::GUID_s& data); diff --git a/include/fastdds_monitor/backend/backend_types.h b/include/fastdds_monitor/backend/backend_types.h index 13a256d7..660c689d 100644 --- a/include/fastdds_monitor/backend/backend_types.h +++ b/include/fastdds_monitor/backend/backend_types.h @@ -39,6 +39,7 @@ 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 AlertKind = eprosima::statistics_backend::AlertKind; using EntityInfo = 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..359c8601 100644 --- a/include/fastdds_monitor/backend/backend_utils.h +++ b/include/fastdds_monitor/backend/backend_utils.h @@ -107,6 +107,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/qml/main.qml b/qml/main.qml index 03c742e5..817871c2 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -123,8 +123,8 @@ ApplicationWindow { AlertKindDialog { id: alertKindDialog onCreateAlert: { - if (alertKind === "NEW_DATA_ON_TOPIC") newDataAlertDialog.open() - else if (alertKind === "NO_DATA_ON_TOPIC") noDataAlertDialog.open() + if (alertKind === "NEW_DATA") newDataAlertDialog.open() + else if (alertKind === "NO_DATA") noDataAlertDialog.open() } } diff --git a/src/Controller.cpp b/src/Controller.cpp index cef4ac6b..34767318 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -295,14 +295,15 @@ void Controller::set_alias( backend::string_to_entity_kind(entity_kind)); } - //! Sets a no data alert - void set_no_data_alert( - QString entity_id, double threshold); - - //! Sets a new data alert - void set_new_data_alert( - QString topic_id); - +void Controller::set_alert( + QString alert_name, + QString entity_id, + QString alert_type, + double threshold) +{ + engine_->set_alert(utils::to_string(alert_name), backend::models_id_to_backend_id(entity_id), + backend::string_to_alert_kind(alert_type), threshold); +} QString Controller::get_data_kind_units( QString data_kind) diff --git a/src/Engine.cpp b/src/Engine.cpp index bedbd5f4..af07e1c0 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -104,6 +104,9 @@ QObject* Engine::enable() entity_status_proxy_model_ = new models::StatusTreeModel(); entity_status_proxy_model_->set_source_model(entity_status_model_); + // Creates a default json structure for alerts and fills the tree model with it + entity_status_model_ = new models::StatusTreeModel(); + update_entity_status(backend::ID_ALL, backend::AlertKind::NONE); source_entity_id_model_ = new models::ListModel(new models::EntityItem()); fill_available_entity_id_list_(backend::EntityKind::HOST, "getDataDialogSourceEntityId"); @@ -1117,6 +1120,8 @@ bool Engine::read_callback_( // Add callback to log model add_log_callback_("New alert reported!", utils::now()); + // DO SOMETHING WITH THE GRAPHIC LAYOUT + return true; } @@ -1407,6 +1412,132 @@ bool Engine::update_entity_status( return true; } +bool Engine::update_entity_alert( + const backend::EntityId& id, + backend::AlertKind kind) +{ + int counter = 0; + if (id == backend::ID_ALL) + { + auto empty_item = new models::StatusTreeItem(backend::ID_ALL, + std::string("No alerts found"), backend::StatusLevel::OK_STATUS, std::string(""), std::string("")); + entity_alert_model_->addTopLevelItem(empty_item); + } + else + { + backend::StatusLevel new_status = backend::StatusLevel::OK_STATUS; + std::string description = backend::entity_alert_description(kind); + std::string entity_guid = backend_connection_.get_guid(id); + std::string entity_kind = utils::to_string(backend::entity_kind_to_QString(backend_connection_.get_type(id))); + switch (kind) + { + case backend::AlertKind::NO_DATA: + { + backend::DeadlineMissedSample sample; + if (backend_connection_.get_status_data(id, sample)) + { + if (sample.status != backend::StatusLevel::OK_STATUS) + { + backend::StatusLevel entity_status = backend_connection_.get_status(id); + auto entity_item = entity_status_model_->getTopLevelItem( + id, entity_kind + ": " + backend_connection_.get_name( + id), entity_status, description, entity_guid); + new_status = sample.status; + std::string handle_string; + auto deadline_missed_item = new models::StatusTreeItem(id, kind, std::string("Deadline missed"), + sample.status, std::string(""), description); + auto total_count_item = new models::StatusTreeItem(id, kind, std::string("Total count:"), + sample.status, std::to_string( + sample.deadline_missed_status.total_count()), std::string("")); + for (uint8_t handler : sample.deadline_missed_status.last_instance_handle()) + { + handle_string = handle_string + std::to_string(handler); + } + auto last_instance_handle_item = new models::StatusTreeItem(id, kind, + std::string("Last instance handle:"), sample.status, handle_string, + std::string("")); + entity_status_model_->addItem(deadline_missed_item, total_count_item); + entity_status_model_->addItem(deadline_missed_item, last_instance_handle_item); + entity_status_model_->addItem(entity_item, deadline_missed_item); + counter = entity_item->recalculate_entity_counter(sample.status); + } + } + break; + } + case backend::StatusKind::NEW_DATA: + { + // backend::InconsistentTopicSample sample; + // if (backend_connection_.get_status_data(id, sample)) + // { + // if (sample.status != backend::StatusLevel::OK_STATUS) + // { + // backend::StatusLevel entity_status = backend_connection_.get_status(id); + // auto entity_item = entity_status_model_->getTopLevelItem( + // id, entity_kind + ": " + backend_connection_.get_name( + // id), entity_status, description, entity_guid); + // new_status = sample.status; + // auto inconsistent_topic_item = + // new models::StatusTreeItem(id, kind, std::string("Inconsistent topics:"), + // sample.status, std::to_string( + // sample.inconsistent_topic_status.total_count()), description); + // entity_status_model_->addItem(entity_item, inconsistent_topic_item); + // counter = entity_item->recalculate_entity_counter(sample.status); + // } + // } + break; + } + default: + { + // No entity status updates, as always returns OK + break; + } + } + if (new_status != backend::StatusLevel::OK_STATUS) + { + // Update entity errors and warnings counters + if (new_status == backend::StatusLevel::ERROR_STATUS) + { + std::map::iterator it = controller_->status_counters.errors.find(id); + if (it != controller_->status_counters.errors.end()) + { + controller_->status_counters.total_errors -= controller_->status_counters.errors[id]; + } + controller_->status_counters.errors[id] = counter; + controller_->status_counters.total_errors += controller_->status_counters.errors[id]; + } + else if (new_status == backend::StatusLevel::WARNING_STATUS) + { + std::map::iterator it = controller_->status_counters.warnings.find(id); + if (it != controller_->status_counters.warnings.end()) + { + controller_->status_counters.total_warnings -= controller_->status_counters.warnings[id]; + } + controller_->status_counters.warnings[id] = counter; + controller_->status_counters.total_warnings += controller_->status_counters.warnings[id]; + } + // notify status model layout changed to refresh layout view + emit entity_status_proxy_model_->layoutAboutToBeChanged(); + + emit controller_->update_status_counters( + QString::number(controller_->status_counters.total_errors), + QString::number(controller_->status_counters.total_warnings)); + + // remove empty message if exists + if (entity_status_model_->is_empty()) + { + entity_status_model_->removeEmptyItem(); + } + + // update view + entity_status_proxy_model_->set_source_model(entity_status_model_); + + // notify status model layout changed to refresh layout view + emit entity_status_proxy_model_->layoutChanged(); + } + } + return true; +} + bool Engine::remove_inactive_entities_from_status_model( const backend::EntityId& id) { diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 90abccbf..52f8c822 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -1801,8 +1801,8 @@ std::vector SyncBackendConnection::get_data_kinds() std::vector SyncBackendConnection::get_alert_kinds() { return std::vector({ - "NEW_DATA_ON_TOPIC", - "NO_DATA_ON_TOPIC"}); + "NEW_DATA", + "NO_DATA"}); } std::vector> SyncBackendConnection::get_data_supported_entity_kinds( diff --git a/src/backend/backend_utils.cpp b/src/backend/backend_utils.cpp index 63e88d50..eb4c5854 100644 --- a/src/backend/backend_utils.cpp +++ b/src/backend/backend_utils.cpp @@ -294,6 +294,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}, + {"NEW_DATA", AlertKind::NEW_DATA}, + }; + + auto it = conversionTable.find(utils::to_string(alert_kind)); + if (it != conversionTable.end()) + { + return it->second; + } + else + { + return AlertKind::NONE; + } +} + std::string get_info_value( const EntityInfo& info, const std::string& key) @@ -530,6 +549,20 @@ std::string entity_status_description( } } +std::string entity_alert_description( + const backend::AlertKind kind) +{ + switch (kind){ + case backend::AlertKind::NO_DATA: + return "No data has been received for the entity in the defined time period"; + case backend::AlertKind::NEW_DATA: + return "New data on the entity has been received"; + default: + case backend::AlertKind::NONE: + return ""; + } +} + std::string policy_documentation_description( const uint32_t& id) { From 389735c78151ed24337706db77c1ab36ff5e9d2e Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Fri, 19 Sep 2025 10:39:10 +0200 Subject: [PATCH 07/42] Refs #23615, simplyfing interface to show alert messages Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Engine.h | 67 ++++-- .../fastdds_monitor/backend/AlertCallback.h | 14 +- include/fastdds_monitor/backend/Listener.h | 6 + .../fastdds_monitor/backend/backend_utils.h | 2 +- qml/AlertList.qml | 9 +- qml/StatusLayout.qml | 43 ++-- src/Controller.cpp | 4 +- src/Engine.cpp | 214 +++++++----------- src/backend/Listener.cpp | 9 + 9 files changed, 186 insertions(+), 182 deletions(-) diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 3caf0a06..e93d3fbb 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -282,16 +282,6 @@ class Engine : public QQmlApplicationEngine const backend::EntityId& id, backend::StatusKind kind); - /** - * @brief Update the alert model with the status kind received - * - * @param id entity id - * @param kind AlertKind reported - * @return true if any change in model has been done - */ - bool update_entity_alert( - const backend::EntityId& id, - backend::AlertKind kind); /** * @brief Update the entity status counters and populate the model with empty message if empty @@ -722,11 +712,35 @@ public slots: */ bool fill_status_(); + /** + * @brief Clear and fill the Alert Model + * + * @return true if any change in any model has been done + */ + bool fill_alert_(); + + /** + * @brief Clear and fill the Alert Message Model + * + * @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); + //! Add a new alert message to the Alert model + bool add_alert_info_( + std::string alert, + std::string time); + + //! Add a new alert message to the Alert Message model + bool add_alert_message_info_( + std::string alert, + std::string time); + //! Add a new issue message to the Issue model bool add_issue_info_( std::string issue, @@ -737,6 +751,20 @@ public slots: std::string name, std::string time); + /** + * Generates a new alert info model from the main schema + * The alert model schema has: + * - "Alerts" tag - to collect alerts + */ + void generate_new_alert_info_(); + + /** + * 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: @@ -760,6 +788,7 @@ public slots: */ void generate_new_status_info_(); + //! Update the issue model "Entities" count adding \c n void sum_entity_number_issue( int n); @@ -825,6 +854,9 @@ public slots: //! Clear issues panel information void clear_issue_info_(); + //! Clear alerts panel information + void clear_alert_info_(); + ///// // Variables @@ -855,6 +887,18 @@ 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::TreeModel* alert_model_; + + //! Data that is represented in the Alert Model when this model is refreshed + 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_; @@ -873,9 +917,6 @@ public slots: //! Display and allow to filter Model for Fast DDS Monitor status view. models::StatusTreeModel* entity_status_proxy_model_; - //! Data Model for Fast DDS Monitor alert view. Collects all alerts detected by the monitor service - models::StatusTreeModel* entity_alert_model_; - //! TODO models::ListModel* source_entity_id_model_; diff --git a/include/fastdds_monitor/backend/AlertCallback.h b/include/fastdds_monitor/backend/AlertCallback.h index 2faae54e..aa5df370 100644 --- a/include/fastdds_monitor/backend/AlertCallback.h +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -26,10 +26,6 @@ namespace backend { -/* - * Struct that store the alert callback information required by the GUI. - * It encapsulates the domain id, entity id and the kind of the new alert reported. - */ struct AlertCallback { //! Void constructor to use copy constructor afterwards @@ -41,10 +37,10 @@ struct AlertCallback AlertCallback( backend::EntityId domain_entity_id, backend::EntityId entity_id, - backend::StatusKind status_kind) + backend::AlertKind alert_kind) : domain_entity_id(domain_entity_id) , entity_id(entity_id) - , status_kind(status_kind) + , alert_kind(alert_kind) { } @@ -52,10 +48,10 @@ struct AlertCallback backend::EntityId domain_entity_id; //! Information of the \c EntityId the callback refers backend::EntityId entity_id; - //! Information of the \c StatusKind the callback refers - backend::StatusKind status_kind; + //! Information of the \c AlertKind the callback refers + backend::AlertKind alert_kind; }; } // namespace backend -#endif // _EPROSIMA_FASTDDS_MONITOR_BACKEND_STATUS_CALLBACK_H +#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..9073d973 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -97,6 +97,12 @@ class Listener : public PhysicalListener EntityId entity_id, StatusKind data_kind) override; + //! Callback when an alert is reported + void on_alert_reported( + EntityId domain_id, + EntityId entity_id, + AlertKind data_kind) override; + protected: //! Engine reference diff --git a/include/fastdds_monitor/backend/backend_utils.h b/include/fastdds_monitor/backend/backend_utils.h index 359c8601..6982f4f2 100644 --- a/include/fastdds_monitor/backend/backend_utils.h +++ b/include/fastdds_monitor/backend/backend_utils.h @@ -109,7 +109,7 @@ backend::StatisticKind string_to_statistic_kind( //! Retrieves the \c AlertKind related with its name in QString backend::AlertKind string_to_alert_kind( - const QString& alert_kind) + const QString& alert_kind); //! recursive function to convert array json subelements to dictionaries indexed by numbers backend::EntityInfo refactor_json( diff --git a/qml/AlertList.qml b/qml/AlertList.qml index 25d52c28..3ea089ca 100644 --- a/qml/AlertList.qml +++ b/qml/AlertList.qml @@ -21,7 +21,9 @@ import QtQuick.Layouts 1.15 import QtQml.Models 2.15 import Theme 1.0 + Rectangle { + id: alertList Layout.fillHeight: true Layout.fillWidth: true @@ -70,10 +72,11 @@ Rectangle { id: participantListColumn Rectangle { + id: participantHighlightRect width: alertList.width height: participantIcon.height - color: highligthRow(clicked) + color: clicked ? Theme.eProsimaLightBlue : "transparent" MouseArea { anchors.fill: parent @@ -153,7 +156,7 @@ Rectangle { id: endpointHighlightRect width: alertList.width height: endpointIcon.height - color: highligthRow(clicked) + color: clicked ? Theme.eProsimaLightBlue : "transparent" MouseArea { anchors.fill: parent @@ -237,7 +240,7 @@ Rectangle { id: locatorHighlightRect width: alertList.width height: locatorIcon.height - color: highligthRow(clicked) + color: clicked ? Theme.eProsimaLightBlue : "transparent" MouseArea { anchors.fill: parent diff --git a/qml/StatusLayout.qml b/qml/StatusLayout.qml index cb0454b3..0f9850d9 100644 --- a/qml/StatusLayout.qml +++ b/qml/StatusLayout.qml @@ -107,29 +107,34 @@ Item color: "white" // Main content of alerts tab: alert tree view with alerts per entity - StatusTreeView { - id: alerts_tree_view + TreeView { + id: alertMessagesView anchors.fill: parent - anchors.margins: 1 - - model: entityStatusModel // problems model: entity status proxy model - - // display if hidden when alerts filtered (from right-click dialog) - onEntity_status_filtered:{ - collapse_alerts_layout() + 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 : "" + } + } } - // filter and clean filter signal-slots management - Connections { - target: alertsLayout - - function onClean_filter_() { - alerts_tree_view.clean_filter() - } + TableViewColumn { + width: parent.width / 2 + role: "name" + title: "Name" + } - function onFocus_entity_(entityId) { - alerts_tree_view.filter_model_by_id(entityId) - } + TableViewColumn { + width: parent.width / 2 + role: "value" + title: "Value" } } } diff --git a/src/Controller.cpp b/src/Controller.cpp index 34767318..13b4579b 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -301,8 +301,8 @@ void Controller::set_alert( QString alert_type, double threshold) { - engine_->set_alert(utils::to_string(alert_name), backend::models_id_to_backend_id(entity_id), - backend::string_to_alert_kind(alert_type), threshold); + // engine_->set_alert(utils::to_string(alert_name), backend::models_id_to_backend_id(entity_id), + // backend::string_to_alert_kind(alert_type), threshold); } QString Controller::get_data_kind_units( diff --git a/src/Engine.cpp b/src/Engine.cpp index af07e1c0..b68fbe07 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -91,6 +91,16 @@ 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::TreeModel(); + generate_new_alert_info_(); + fill_alert_(); + + // 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,10 +114,6 @@ QObject* Engine::enable() entity_status_proxy_model_ = new models::StatusTreeModel(); entity_status_proxy_model_->set_source_model(entity_status_model_); - // Creates a default json structure for alerts and fills the tree model with it - entity_status_model_ = new models::StatusTreeModel(); - update_entity_status(backend::ID_ALL, backend::AlertKind::NONE); - 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()); @@ -134,6 +140,8 @@ 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("entityModelFirst", source_entity_id_model_); rootContext()->setContextProperty("entityModelSecond", destination_entity_id_model_); @@ -237,6 +245,16 @@ Engine::~Engine() delete entity_status_proxy_model_; } + if (alert_model_) + { + delete alert_model_; + } + + if (alert_message_model_) + { + delete alert_message_model_; + } + // Auxiliar models if (source_entity_id_model_) { @@ -411,6 +429,18 @@ bool Engine::fill_issue_() return true; } +bool Engine::fill_alert_() +{ + alert_model_->update(alert_info_); + return true; +} + +bool Engine::fill_alert_message_() +{ + alert_message_model_->update(alert_message_info_); + return true; +} + bool Engine::fill_log_() { log_model_->update(log_info_); @@ -423,6 +453,24 @@ bool Engine::fill_status_() return true; } +void Engine::generate_new_alert_info_() +{ + EntityInfo info; + + info["Alerts"] = EntityInfo(); + + alert_info_ = info; +} + +void Engine::generate_new_alert_message_info_() +{ + EntityInfo info; + + info["Messages"] = EntityInfo(); + + alert_message_info_ = info; +} + void Engine::generate_new_issue_info_() { EntityInfo info; @@ -502,6 +550,32 @@ void Engine::clear_issue_info_() fill_issue_(); } +bool Engine::add_alert_info_( + std::string alert, + std::string time) +{ + alert_info_["Alerts"][time] = alert; + fill_alert_(); + + return true; +} + +bool Engine::add_alert_message_info_( + std::string alert, + std::string time) +{ + alert_message_info_["Alerts"][time] = alert; + fill_alert_message_(); + + return true; +} + +void Engine::clear_alert_info_() +{ + alert_info_["Alerts"] = EntityInfo(); + fill_alert_(); +} + bool Engine::fill_first_entity_info_() { EntityInfo info = R"({"No monitors active.":"Start a monitor in a specific domain"})"_json; @@ -1118,11 +1192,7 @@ bool Engine::read_callback_( std::lock_guard lock(initializing_monitor_); // Add callback to log model - add_log_callback_("New alert reported!", utils::now()); - - // DO SOMETHING WITH THE GRAPHIC LAYOUT - - return true; + return add_alert_message_info_("New alert reported!", utils::now()); } bool Engine::update_entity_status( @@ -1412,132 +1482,6 @@ bool Engine::update_entity_status( return true; } -bool Engine::update_entity_alert( - const backend::EntityId& id, - backend::AlertKind kind) -{ - int counter = 0; - if (id == backend::ID_ALL) - { - auto empty_item = new models::StatusTreeItem(backend::ID_ALL, - std::string("No alerts found"), backend::StatusLevel::OK_STATUS, std::string(""), std::string("")); - entity_alert_model_->addTopLevelItem(empty_item); - } - else - { - backend::StatusLevel new_status = backend::StatusLevel::OK_STATUS; - std::string description = backend::entity_alert_description(kind); - std::string entity_guid = backend_connection_.get_guid(id); - std::string entity_kind = utils::to_string(backend::entity_kind_to_QString(backend_connection_.get_type(id))); - switch (kind) - { - case backend::AlertKind::NO_DATA: - { - backend::DeadlineMissedSample sample; - if (backend_connection_.get_status_data(id, sample)) - { - if (sample.status != backend::StatusLevel::OK_STATUS) - { - backend::StatusLevel entity_status = backend_connection_.get_status(id); - auto entity_item = entity_status_model_->getTopLevelItem( - id, entity_kind + ": " + backend_connection_.get_name( - id), entity_status, description, entity_guid); - new_status = sample.status; - std::string handle_string; - auto deadline_missed_item = new models::StatusTreeItem(id, kind, std::string("Deadline missed"), - sample.status, std::string(""), description); - auto total_count_item = new models::StatusTreeItem(id, kind, std::string("Total count:"), - sample.status, std::to_string( - sample.deadline_missed_status.total_count()), std::string("")); - for (uint8_t handler : sample.deadline_missed_status.last_instance_handle()) - { - handle_string = handle_string + std::to_string(handler); - } - auto last_instance_handle_item = new models::StatusTreeItem(id, kind, - std::string("Last instance handle:"), sample.status, handle_string, - std::string("")); - entity_status_model_->addItem(deadline_missed_item, total_count_item); - entity_status_model_->addItem(deadline_missed_item, last_instance_handle_item); - entity_status_model_->addItem(entity_item, deadline_missed_item); - counter = entity_item->recalculate_entity_counter(sample.status); - } - } - break; - } - case backend::StatusKind::NEW_DATA: - { - // backend::InconsistentTopicSample sample; - // if (backend_connection_.get_status_data(id, sample)) - // { - // if (sample.status != backend::StatusLevel::OK_STATUS) - // { - // backend::StatusLevel entity_status = backend_connection_.get_status(id); - // auto entity_item = entity_status_model_->getTopLevelItem( - // id, entity_kind + ": " + backend_connection_.get_name( - // id), entity_status, description, entity_guid); - // new_status = sample.status; - // auto inconsistent_topic_item = - // new models::StatusTreeItem(id, kind, std::string("Inconsistent topics:"), - // sample.status, std::to_string( - // sample.inconsistent_topic_status.total_count()), description); - // entity_status_model_->addItem(entity_item, inconsistent_topic_item); - // counter = entity_item->recalculate_entity_counter(sample.status); - // } - // } - break; - } - default: - { - // No entity status updates, as always returns OK - break; - } - } - if (new_status != backend::StatusLevel::OK_STATUS) - { - // Update entity errors and warnings counters - if (new_status == backend::StatusLevel::ERROR_STATUS) - { - std::map::iterator it = controller_->status_counters.errors.find(id); - if (it != controller_->status_counters.errors.end()) - { - controller_->status_counters.total_errors -= controller_->status_counters.errors[id]; - } - controller_->status_counters.errors[id] = counter; - controller_->status_counters.total_errors += controller_->status_counters.errors[id]; - } - else if (new_status == backend::StatusLevel::WARNING_STATUS) - { - std::map::iterator it = controller_->status_counters.warnings.find(id); - if (it != controller_->status_counters.warnings.end()) - { - controller_->status_counters.total_warnings -= controller_->status_counters.warnings[id]; - } - controller_->status_counters.warnings[id] = counter; - controller_->status_counters.total_warnings += controller_->status_counters.warnings[id]; - } - // notify status model layout changed to refresh layout view - emit entity_status_proxy_model_->layoutAboutToBeChanged(); - - emit controller_->update_status_counters( - QString::number(controller_->status_counters.total_errors), - QString::number(controller_->status_counters.total_warnings)); - - // remove empty message if exists - if (entity_status_model_->is_empty()) - { - entity_status_model_->removeEmptyItem(); - } - - // update view - entity_status_proxy_model_->set_source_model(entity_status_model_); - - // notify status model layout changed to refresh layout view - emit entity_status_proxy_model_->layoutChanged(); - } - } - return true; -} - bool Engine::remove_inactive_entities_from_status_model( const backend::EntityId& id) { diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index 26b57379..4272d83b 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -177,4 +177,13 @@ void Listener::on_status_reported( engine_->add_callback(StatusCallback(domain_id, entity_id, status_kind)); } +void Listener::on_alert_reported( + EntityId domain_id, + EntityId entity_id, + AlertKind alert_kind) +{ + engine_->add_callback(AlertCallback(domain_id, entity_id, alert_kind)); +} + + } //namespace backend From fc6f230b2135bed38426444af94aef2f4ede104e Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Fri, 19 Sep 2025 11:41:11 +0200 Subject: [PATCH 08/42] Adjusting left panel alert view Signed-off-by: Emilio Cuesta --- qml/AlertList.qml | 16 -------- qml/AlertsPanel.qml | 98 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/qml/AlertList.qml b/qml/AlertList.qml index 3ea089ca..1b097102 100644 --- a/qml/AlertList.qml +++ b/qml/AlertList.qml @@ -99,22 +99,6 @@ Rectangle { } } } - - RowLayout { - spacing: spacingIconLabel - - IconSVG { - id: participantIcon - name: "participant" - size: iconSize - Layout.leftMargin: firstIndentation - color: entityLabelColor(clicked, alive) - } - Label { - text: name - color: entityLabelColor(clicked, alive) - } - } } ListView { diff --git a/qml/AlertsPanel.qml b/qml/AlertsPanel.qml index 3a2dc10c..879a6988 100644 --- a/qml/AlertsPanel.qml +++ b/qml/AlertsPanel.qml @@ -76,7 +76,7 @@ ColumnLayout { delegate: MenuItem { id: menuItem implicitWidth: 150 - implicitHeight: contextMenu.height + implicitHeight: 30 indicator: Item { implicitWidth: 30 @@ -120,18 +120,90 @@ ColumnLayout { Layout.fillHeight: true Layout.fillWidth: true - ColumnLayout { - id: alertListLayout - SplitView.preferredHeight: parent.height / 4 - spacing: 10 - visible: true - clip: true - - AlertList { - id: alertList - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - Layout.bottomMargin: 1 + 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: alertInfo + visible: true + SplitView.fillHeight: true + SplitView.preferredHeight: parent.height / 4 + SplitView.minimumHeight: infoTabBar.height + clip: true + + Rectangle { + id: infoSelectedAlert + property string alert_id: "UNKNOWN_ALERT" + anchors.top: infoTabBar.bottom + anchors.left: parent.left + width: parent.width + height: infoTabBar.height + + TabBar { + id: infoTabBar + anchors.top: parent.top + anchors.left: parent.left + width: parent.width + TabButton { + text: qsTr("Info") + } + } + + Rectangle + { + color: "transparent" + anchors.top: infoTabBar.bottom + height: infoTabBar.height + width: parent.width + + Label { + id: infoSelectedEntityLabel + text: "No entity selected" + font.pointSize: 10 + font.italic: true + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + } } } } From dbad82a21b444d6fa59eff1733f5d502e127529b Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Sun, 21 Sep 2025 13:49:16 +0200 Subject: [PATCH 09/42] Refs #23615, full alert pipe ready Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 2 +- include/fastdds_monitor/Engine.h | 5 +++++ .../backend/SyncBackendConnection.h | 10 ++++++---- include/fastdds_monitor/backend/backend_types.h | 3 +++ qml/AlertList.qml | 2 +- qml/LeftPanel.qml | 2 +- src/Controller.cpp | 5 +++-- src/Engine.cpp | 12 ++++++++++++ src/backend/SyncBackendConnection.cpp | 16 ++++++++++++++++ 9 files changed, 48 insertions(+), 9 deletions(-) diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 994d5031..192f7426 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -229,7 +229,7 @@ public slots: QString new_alias, QString entity_kind); - //! Sets a no data alert + //! Adds a new alert void set_alert( QString alert_name, QString entity_id, diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index e93d3fbb..4b93d498 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -485,6 +485,11 @@ class Engine : public QQmlApplicationEngine const std::string& new_alias, const backend::EntityKind& entity_kind); + void set_alert( + const std::string& alert_name, + const backend::AlertKind& alert_kind, + const double& threshold); + /** * This methods updates the info and summary if the entity clicked (the entity that is being shown) is the * entity updated. diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 636a0249..1e709fb5 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -225,10 +225,6 @@ class SyncBackendConnection EntityId source_entity_id, ExtendedIncompatibleQosSample& sample); - bool get_alert_data( - EntityId source_entity_id, - Alert& sample); - //! Convert a given entity guid to string format std::string get_deserialized_guid( const backend::GUID_s& data); @@ -846,6 +842,12 @@ 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 backend::AlertKind& alert_kind, + double threshold); + protected: ListModel* get_model_( diff --git a/include/fastdds_monitor/backend/backend_types.h b/include/fastdds_monitor/backend/backend_types.h index 660c689d..c0154b94 100644 --- a/include/fastdds_monitor/backend/backend_types.h +++ b/include/fastdds_monitor/backend/backend_types.h @@ -25,11 +25,13 @@ #include +#include #include #include #include #include + namespace backend { //! Add a type of each kind with same name under \c backend namespace @@ -40,6 +42,7 @@ using StatusKind = eprosima::statistics_backend::StatusKind; using StatusLevel = eprosima::statistics_backend::StatusLevel; using StatisticKind = eprosima::statistics_backend::StatisticKind; using AlertKind = eprosima::statistics_backend::AlertKind; +using AlertInfo = eprosima::statistics_backend::Info; using EntityInfo = eprosima::statistics_backend::Info; using Timestamp = eprosima::statistics_backend::Timestamp; using GUID_s = eprosima::fastdds::statistics::detail::GUID_s; diff --git a/qml/AlertList.qml b/qml/AlertList.qml index 1b097102..549ba18b 100644 --- a/qml/AlertList.qml +++ b/qml/AlertList.qml @@ -75,7 +75,7 @@ Rectangle { id: participantHighlightRect width: alertList.width - height: participantIcon.height + height: participantItem.height color: clicked ? Theme.eProsimaLightBlue : "transparent" MouseArea { diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index c39eb5f5..c39b53e1 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -206,6 +206,6 @@ RowLayout { function createNewDataAlert(topicId) { // TODO: Remove console.log("MOCK: Creating new data alert for topic " + topicId) - controller.set_new_data_alert(topicId) + controller.set_alert("test2", topicId, "NEW_DATA", 0); } } diff --git a/src/Controller.cpp b/src/Controller.cpp index 13b4579b..88b4edf8 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -301,8 +301,9 @@ void Controller::set_alert( QString alert_type, double threshold) { - // engine_->set_alert(utils::to_string(alert_name), backend::models_id_to_backend_id(entity_id), - // backend::string_to_alert_kind(alert_type), threshold); + engine_->set_alert(utils::to_string(alert_name), + backend::string_to_alert_kind(alert_type), + threshold); } QString Controller::get_data_kind_units( diff --git a/src/Engine.cpp b/src/Engine.cpp index b68fbe07..63e1f0f7 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -1813,6 +1813,18 @@ void Engine::set_alias( } } +void Engine::set_alert( + const std::string& alert_name, + const backend::AlertKind& alert_kind, + const double& threshold) +{ + // Adding alert to backend structures + backend_connection_.set_alert(alert_name, alert_kind, threshold); + // Adding alert to engine and GUI structures + // NOTE: We cannot do this if we don't know the backend ID maybe + add_alert_info_(alert_name, utils::now()); +} + bool Engine::update_entity( const backend::EntityId& entity_updated, bool (Engine::* update_function)(const backend::EntityId&, bool, bool), diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 52f8c822..f8e14f71 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -1222,6 +1222,22 @@ void SyncBackendConnection::set_alias( } } +void SyncBackendConnection::set_alert( + const std::string& alert_name, + const backend::AlertKind& alert_kind, + double threshold) +{ + try + { + StatisticsBackend::set_alert(alert_name, alert_kind, threshold); + } + catch (const Exception& e) + { + qWarning() << "Fail setting new alert"; + static_cast(e); + } +} + bool SyncBackendConnection::update_host( models::ListModel* physical_model, EntityId id, From 7b58620d97e83b1fbd6a14d17b881f5cc8825c89 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Mon, 22 Sep 2025 09:59:12 +0200 Subject: [PATCH 10/42] Changing on_alert_reported by triggered Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/backend/Listener.h | 2 +- src/backend/Listener.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index 9073d973..c9b30721 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -98,7 +98,7 @@ class Listener : public PhysicalListener StatusKind data_kind) override; //! Callback when an alert is reported - void on_alert_reported( + void on_alert_triggered( EntityId domain_id, EntityId entity_id, AlertKind data_kind) override; diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index 4272d83b..adb03a58 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -177,7 +177,7 @@ void Listener::on_status_reported( engine_->add_callback(StatusCallback(domain_id, entity_id, status_kind)); } -void Listener::on_alert_reported( +void Listener::on_alert_triggered( EntityId domain_id, EntityId entity_id, AlertKind alert_kind) From 623e0800d1f6785b2206dd55193ee85b5ee00651 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Fri, 26 Sep 2025 16:09:07 +0200 Subject: [PATCH 11/42] Display does not collapse now Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 12 +- include/fastdds_monitor/Engine.h | 22 ++- .../fastdds_monitor/backend/AlertCallback.h | 24 ++- include/fastdds_monitor/backend/Listener.h | 6 +- .../backend/SyncBackendConnection.h | 7 +- .../fastdds_monitor/backend/backend_types.h | 2 +- include/fastdds_monitor/model/tree/TreeItem.h | 3 + .../fastdds_monitor/model/tree/TreeModel.h | 22 +++ qml/AlertsPanel.qml | 60 +------ qml/LeftPanel.qml | 22 ++- qml/NewDataAlertDialog.qml | 158 ++++++++++++++---- qml/NoDataAlertDialog.qml | 21 +-- qml/Panels.qml | 8 +- qml/StatusLayout.qml | 17 +- qml/main.qml | 4 +- src/Controller.cpp | 29 +++- src/Engine.cpp | 96 +++++++++-- src/backend/Listener.cpp | 5 +- src/backend/SyncBackendConnection.cpp | 9 +- src/model/tree/TreeItem.cpp | 6 + src/model/tree/TreeModel.cpp | 131 +++++++++++++++ 21 files changed, 499 insertions(+), 165 deletions(-) diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 192f7426..b4052cc9 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -231,10 +231,14 @@ public slots: //! Adds a new alert void set_alert( - QString alert_name, - QString entity_id, - QString alert_type, - double threshold); + QString alert_name, + QString host_name, + QString user_name, + QString topic_name, + QString alert_type, + double threshold, + int time_between_triggers, + QString contact_info); //! Give a string with the name of the unit magnitud in which each DataKind is measured QString get_data_kind_units( diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 4b93d498..91f6e0e3 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -487,8 +487,13 @@ class Engine : public QQmlApplicationEngine void set_alert( const std::string& alert_name, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, const backend::AlertKind& alert_kind, - const double& threshold); + double threshold, + const std::chrono::milliseconds& t_between_triggers, + const std::string& contact_info); /** * This methods updates the info and summary if the entity clicked (the entity that is being shown) is the @@ -742,9 +747,10 @@ public slots: std::string time); //! Add a new alert message to the Alert Message model - bool add_alert_message_info_( - std::string alert, - std::string time); +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_( @@ -928,8 +934,12 @@ public slots: //! TODO models::ListModel* destination_entity_id_model_; - //! Model to hold the data about the alerts created - models::ListModel* alert_entity_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_; diff --git a/include/fastdds_monitor/backend/AlertCallback.h b/include/fastdds_monitor/backend/AlertCallback.h index aa5df370..62e1f1fd 100644 --- a/include/fastdds_monitor/backend/AlertCallback.h +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -23,33 +23,31 @@ #define _EPROSIMA_FASTDDS_MONITOR_BACKEND_ALERT_CALLBACK_H #include +#include + namespace backend { struct AlertCallback { - //! Void constructor to use copy constructor afterwards - AlertCallback() - { - } + AlertCallback() = default; - //! Standard constructor with the two fields required AlertCallback( backend::EntityId domain_entity_id, backend::EntityId entity_id, - backend::AlertKind alert_kind) - : domain_entity_id(domain_entity_id) + backend::AlertInfo alert_info, + double trigger_data) + : domain_id(domain_entity_id) , entity_id(entity_id) - , alert_kind(alert_kind) + , alert_info(alert_info) + , trigger_data(trigger_data) { } - //! Information of the domain \c EntityId the callback refers - backend::EntityId domain_entity_id; - //! Information of the \c EntityId the callback refers + backend::EntityId domain_id; backend::EntityId entity_id; - //! Information of the \c AlertKind the callback refers - backend::AlertKind alert_kind; + backend::AlertInfo alert_info; + double trigger_data; }; } // namespace backend diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index c9b30721..c827645c 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -97,11 +97,13 @@ class Listener : public PhysicalListener EntityId entity_id, StatusKind data_kind) override; - //! Callback when an alert is reported + //! Callback when an alert is triggered void on_alert_triggered( EntityId domain_id, EntityId entity_id, - AlertKind data_kind) override; + const AlertInfo& alert, + const double& data) override; + protected: diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 1e709fb5..1b700288 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -845,8 +845,13 @@ class SyncBackendConnection //! Set a new alert in backend void set_alert( const std::string& alert_name, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, const backend::AlertKind& alert_kind, - double threshold); + double threshold, + const std::chrono::milliseconds& t_between_triggers, + const std::string& contact_info); protected: diff --git a/include/fastdds_monitor/backend/backend_types.h b/include/fastdds_monitor/backend/backend_types.h index c0154b94..c52cce0c 100644 --- a/include/fastdds_monitor/backend/backend_types.h +++ b/include/fastdds_monitor/backend/backend_types.h @@ -42,7 +42,7 @@ using StatusKind = eprosima::statistics_backend::StatusKind; using StatusLevel = eprosima::statistics_backend::StatusLevel; using StatisticKind = eprosima::statistics_backend::StatisticKind; using AlertKind = eprosima::statistics_backend::AlertKind; -using AlertInfo = eprosima::statistics_backend::Info; +using AlertInfo = eprosima::statistics_backend::AlertInfo; using EntityInfo = 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/model/tree/TreeItem.h b/include/fastdds_monitor/model/tree/TreeItem.h index 7b561ad3..7584e26e 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..66fcdbba 100644 --- a/include/fastdds_monitor/model/tree/TreeModel.h +++ b/include/fastdds_monitor/model/tree/TreeModel.h @@ -111,6 +111,9 @@ 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 +149,25 @@ 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 the children of a node to find one 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/qml/AlertsPanel.qml b/qml/AlertsPanel.qml index 879a6988..e7d67ef5 100644 --- a/qml/AlertsPanel.qml +++ b/qml/AlertsPanel.qml @@ -46,70 +46,16 @@ ColumnLayout { color: Theme.whiteSmoke } IconSVG { - name: "three_dots_menu" + name: "plus" Layout.alignment: Qt.AlignRight - scalingFactor: 2 + scalingFactor: 1.4 color: "white" - MouseArea { anchors.fill: parent onClicked: { - contextMenu.y = parent.y + parent.height; - contextMenu.open() - } - } - - Menu { - id: contextMenu - height: 30 - - Action { - id: contextMenuDDSEntities - text: "Add Alert" - checkable: false - onTriggered: { - alertKindDialog.open() - } - } - delegate: MenuItem { - id: menuItem - implicitWidth: 150 - implicitHeight: 30 - - indicator: Item { - implicitWidth: 30 - implicitHeight: 30 - Rectangle { - width: 16 - height: 16 - anchors.centerIn: parent - visible: menuItem.checkable - border.color: menuItem.highlighted ? Theme.eProsimaLightBlue : - !menuItem.checked ? Theme.grey : "black" - radius: 3 - Rectangle { - width: 10 - height: 10 - anchors.centerIn: parent - visible: menuItem.checked - color: Theme.eProsimaLightBlue - radius: 2 - } - } - } - - contentItem: Text { - leftPadding: 15 - text: menuItem.text - opacity: enabled ? 1.0 : 0.3 - color: menuItem.highlighted ? Theme.eProsimaLightBlue : - !menuItem.checked ? Theme.grey : "black" - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } + alertKindDialog.open() } } } diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index c39b53e1..ae06fbf7 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -196,16 +196,20 @@ RowLayout { monitoringPanel.changeExplorerEntityInfo(status) } - function createNoDataAlert(entityKind, entityId, noDataThreshold) { - // TODO: Remove - console.log("MOCK: Creating no data alert for topic " + topicId) - // panels.createNoDataAlert(entityKind, entityId, noDataThreshold) - // controller.participant_click(entityId) + function createAlert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers) { + console.log("MOCK2: Creating alert for topic " + topicId + " with threshold " + threshold + " and time between triggers " + t_between_triggers) + controller.set_alert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers); } - function createNewDataAlert(topicId) { - // TODO: Remove - console.log("MOCK: Creating new data alert for topic " + topicId) - controller.set_alert("test2", topicId, "NEW_DATA", 0); + function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers) { + // TODO: Remove log + console.log("MOCK: Creating no data alert for topic " + topicId + " with threshold " + threshold + " and time between triggers " + t_between_triggers) + createAlert(name, hostId, userId, topicId, "NO_DATA", threshold, t_between_triggers); + } + + function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers) { + // TODO: Remove log + console.log("MOCK: Creating new data alert for topic " + topicId + " with time between triggers " + t_between_triggers) + createAlert(name, hostId, userId, topicId, "NEW_DATA", 0, t_between_triggers); } } diff --git a/qml/NewDataAlertDialog.qml b/qml/NewDataAlertDialog.qml index 52b9b34c..7f991a47 100644 --- a/qml/NewDataAlertDialog.qml +++ b/qml/NewDataAlertDialog.qml @@ -28,33 +28,44 @@ Dialog { standardButtons: Dialog.Ok | Dialog.Cancel property bool activeOk: true + property string currentAlertName: "" + property string currentHost: "" + property string currentUser: "" property string currentTopic: "" + property int currentThreshold: 5 + property int currentTimeBetweenAlerts: 5000 x: (parent.width - width) / 2 y: (parent.height - height) / 2 - signal createAlert(string topicId) + signal createAlert(string alert_name, string host_name, string user_name, string topic_name, + int t_between_triggers) Component.onCompleted: { standardButton(Dialog.Ok).text = qsTrId("Add") standardButton(Dialog.Cancel).text = qsTrId("Close") - - // Get the available topics from the backend - controller.update_available_entity_ids("Topic", "getDataDialogSourceEntityId") } onAccepted: { if (!checkInputs()) return + currentAlertName = alertNameTextField.text + currentHost = hostComboBox.currentText + currentUser = userComboBox.currentText currentTopic = topicComboBox.currentText - createAlert(currentTopic) + currentTimeBetweenAlerts = noDataTimeBetweenAlerts.value + createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentTimeBetweenAlerts) } onAboutToShow: { - getDataDialogSourceEntityId.currentIndex = 0 - alertTextField.text = "" - controller.update_available_entity_ids("Topic", "getDataDialogSourceEntityId") + alertNameTextField.text = "" + hostComboBox.currentIndex = 0 + topicComboBox.currentIndex = 0 + userComboBox.currentIndex = 0 + updateTopics() + updateUsers() + updateHosts() } GridLayout{ @@ -63,63 +74,136 @@ Dialog { rowSpacing: 20 Label { - id: seriesLabel - text: "Alert label: " + id: alertNameLabel + text: "Alert name: " InfoToolTip { - text: "Name of the alert.\n" + text: "Name of the alert.\n"+ + "The alert name is autogerated\n" + + "using the values given in this\n" + + "dialog." } } + TextField { - id: alertTextField + id: alertNameTextField placeholderText: "" selectByMouse: true - maximumLength: 50 + maximumLength: 100 Layout.fillWidth: true onTextEdited: activeOk = true } + Label { + id: hostLabel + text: "Host: " + InfoToolTip { + text: "Host name from which the data\n" + + "will be collected." + } + } + + AdaptiveComboBox { + id: hostComboBox + textRole: "host" + valueRole: "id" + displayText: currentIndex === -1 + ? ("Please choose a host...") + : currentText + model: alertHostModel + Component.onCompleted: currentIndex = -1 + + onActivated: { + activeOk = true + } + } Label { - id: sourceEntityIdLabel - text: "Source Entity Id: " + id: userLabel + text: "User: " InfoToolTip { - text: "Entity from which the data\n" + + text: "User name from which the data\n" + "will be collected." } } - RowLayout { - AdaptiveComboBox { - id: getDataDialogSourceEntityId - model: ["Topic"] + + AdaptiveComboBox { + id: userComboBox + textRole: "user" + valueRole: "id" + 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 name from which the data\n" + + "will be collected." } - AdaptiveComboBox { + } + + AdaptiveComboBox { id: topicComboBox - textRole: "nameId" + textRole: "topic" valueRole: "id" displayText: currentIndex === -1 - ? ("Please choose a " + getDataDialogSourceEntityId.currentText + "...") + ? ("Please choose a topic...") : currentText - model: entityModelFirst + 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." } } + SpinBox { + id: noDataThreshold + editable: true + from: 1 + to: 100 + stepSize: 1 + value: 5 + } + + Label { + text: "Time between alerts (ms): " + InfoToolTip { + text: "Minimum time between two consecutive alerts." + } + } + SpinBox { + id: noDataTimeBetweenAlerts + editable: true + from: 0 + to: 10000 + stepSize: 50 + value: 5000 + } } MessageDialog { - id: emptyAlertLabel - title: "Missing alert label" + id: emptyAlertName + title: "Missing alert name" icon: StandardIcon.Warning standardButtons: StandardButton.Retry | StandardButton.Discard - text: "The alert label field is empty. Please enter an alert label." + text: "The alert name field is empty. Please enter an alert name." onAccepted: newDataAlertDialog.open() onDiscard: newDataAlertDialog.close() } @@ -139,16 +223,28 @@ Dialog { emptyTopic.open() return false } - if (alertTextField.text === "") { - emptyAlertLabel.open() + if (alertNameTextField.text === "") { + emptyAlertName.open() return false } return true } - function updateEntities() { - controller.update_available_entity_ids("Topic", "getDataDialogSourceEntityId") + function updateTopics() { + controller.update_available_entity_ids("Topic", "alertTopic") } + function updateUsers(){ + controller.update_available_entity_ids("User", "alertUser") + } + + function updateHosts(){ + controller.update_available_entity_ids("Host", "alertHost") + } + + function formatText(count, modelData) { + var data = count === 24 ? modelData + 1 : modelData; + return data.toString().length < 2 ? "0" + data : data; + } } diff --git a/qml/NoDataAlertDialog.qml b/qml/NoDataAlertDialog.qml index af970b25..cc1ff725 100644 --- a/qml/NoDataAlertDialog.qml +++ b/qml/NoDataAlertDialog.qml @@ -32,7 +32,8 @@ Dialog { property bool activeOk: true - signal createAlert(string entityKind, string entityId, int noDataThreshold) + signal createAlert(string alert_name, string host_name, string user_name, string topic_name, + int threshold, int t_between_triggers) Component.onCompleted: { standardButton(Dialog.Ok).text = qsTrId("Add") @@ -51,22 +52,8 @@ Dialog { if (!checkInputs()) return - if (activeOk) { - createAlert(getDataDialogSourceEntityId.currentText, sourceEntityId.currentText, noDataThreshold.value) - } - activeOk = true - } - - onApplied: { - if (!checkInputs()) - return - - if (activeOk) { - createAlert() - } - activeOk = false - statisticKind.currentIndex = -1 - cumulative.checked = false + currentTopic = topicComboBox.currentText + createAlert("fixed_name", "", "", currentTopic, noDataThreshold.value, 5000) } onClosed: activeOk = true diff --git a/qml/Panels.qml b/qml/Panels.qml index 2873dd92..3f63dc2e 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -207,12 +207,12 @@ RowLayout { leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } - function createNewDataAlert(topicId){ - leftPanel.createNewDataAlert(topicId) + function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers){ + leftPanel.createNewDataAlert(name, hostId, userId, topicId, t_between_triggers) } - function createNoDataAlert(entityKind, entityId, noDataThreshold){ - leftPanel.createNoDataAlert(entityKind, entityId, noDataThreshold) + function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers){ + leftPanel.createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers) } } diff --git a/qml/StatusLayout.qml b/qml/StatusLayout.qml index 0f9850d9..911b8b09 100644 --- a/qml/StatusLayout.qml +++ b/qml/StatusLayout.qml @@ -288,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 @@ -331,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/main.qml b/qml/main.qml index 817871c2..7c952b6b 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -131,7 +131,7 @@ ApplicationWindow { NewDataAlertDialog { id: newDataAlertDialog onCreateAlert: { - panels.createNewDataAlert(topicId) + panels.createNewDataAlert(alert_name, host_name, user_name, topic_name, t_between_triggers) } } @@ -139,7 +139,7 @@ ApplicationWindow { NoDataAlertDialog { id: noDataAlertDialog onCreateAlert: { - panels.createNoDataAlert(entityKind, entityId, noDataThreshold) + panels.createNoDataAlert(alert_name, host_name, user_name, topic_name, threshold, t_between_triggers) } } diff --git a/src/Controller.cpp b/src/Controller.cpp index 88b4edf8..e608d8c7 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -295,15 +295,38 @@ 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 entity_id, + QString host_name, + QString user_name, + QString topic_name, QString alert_type, - double threshold) + double threshold, + int time_between_triggers, + QString contact_info) { + + 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), + clean_host_name, + clean_user_name, + clean_topic_name, backend::string_to_alert_kind(alert_type), - threshold); + threshold, + std::chrono::milliseconds(time_between_triggers), + utils::to_string(contact_info)); } QString Controller::get_data_kind_units( diff --git a/src/Engine.cpp b/src/Engine.cpp index 63e1f0f7..c3822c6d 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -119,6 +119,13 @@ QObject* Engine::enable() destination_entity_id_model_ = new models::ListModel(new models::EntityItem()); fill_available_entity_id_list_(backend::EntityKind::HOST, "getDataDialogDestinationEntityId"); + 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(); @@ -145,6 +152,9 @@ QObject* Engine::enable() rootContext()->setContextProperty("entityModelFirst", source_entity_id_model_); rootContext()->setContextProperty("entityModelSecond", destination_entity_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_); @@ -266,6 +276,20 @@ Engine::~Engine() delete destination_entity_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_) { @@ -437,7 +461,7 @@ bool Engine::fill_alert_() bool Engine::fill_alert_message_() { - alert_message_model_->update(alert_message_info_); + alert_message_model_->update_without_collapse(alert_message_info_); return true; } @@ -466,7 +490,7 @@ void Engine::generate_new_alert_message_info_() { EntityInfo info; - info["Messages"] = EntityInfo(); + info = EntityInfo(); alert_message_info_ = info; } @@ -561,10 +585,12 @@ bool Engine::add_alert_info_( } bool Engine::add_alert_message_info_( - std::string alert, + std::string alert_name, + std::string msg, std::string time) { - alert_message_info_["Alerts"][time] = alert; + alert_message_info_[alert_name][time] = msg; + fill_alert_message_(); return true; @@ -572,7 +598,7 @@ bool Engine::add_alert_message_info_( void Engine::clear_alert_info_() { - alert_info_["Alerts"] = EntityInfo(); + alert_info_ = EntityInfo(); fill_alert_(); } @@ -849,6 +875,33 @@ bool Engine::on_selected_entity_kind( 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()); + } + 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()); + } + 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()); + } else { return false; @@ -1187,12 +1240,25 @@ bool Engine::read_callback_( 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_); - // Add callback to log model - return add_alert_message_info_("New alert reported!", utils::now()); + switch (alert_callback.alert_info.get_alert_kind()) + { + case backend::AlertKind::NEW_DATA: + return add_alert_message_info_(alert_callback.alert_info.get_alert_name(), "NEW_DATA alert triggered", utils::now()); + break; + case backend::AlertKind::NO_DATA: + return add_alert_message_info_(alert_callback.alert_info.get_alert_name(), "NO_DATA alert triggered", utils::now()); + break; + case backend::AlertKind::NONE: + default: + // Unknown alerts are ignored + break; + } + + return false; } bool Engine::update_entity_status( @@ -1815,11 +1881,21 @@ void Engine::set_alias( void Engine::set_alert( const std::string& alert_name, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, const backend::AlertKind& alert_kind, - const double& threshold) + double threshold, + const std::chrono::milliseconds& t_between_triggers) { // Adding alert to backend structures - backend_connection_.set_alert(alert_name, alert_kind, threshold); + // backend_connection_.set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, t_between_triggers); + backend_connection_.set_alert(alert_name, "", "", "", alert_kind, threshold, t_between_triggers); + + std::cout << "Alert " << alert_name << " created with host " << host_name << ", user " << user_name + << ", topic " << topic_name + << ", threshold " << threshold << " and time between triggers " << t_between_triggers.count() << " ms" << std::endl; + // Adding alert to engine and GUI structures // NOTE: We cannot do this if we don't know the backend ID maybe add_alert_info_(alert_name, utils::now()); diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index adb03a58..03f6c4c0 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -180,9 +180,10 @@ void Listener::on_status_reported( void Listener::on_alert_triggered( EntityId domain_id, EntityId entity_id, - AlertKind alert_kind) + const AlertInfo &alert, + const double &data) { - engine_->add_callback(AlertCallback(domain_id, entity_id, alert_kind)); + engine_->add_callback(AlertCallback(domain_id, entity_id, alert, data)); } diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index f8e14f71..2202f9f3 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -1224,12 +1224,17 @@ void SyncBackendConnection::set_alias( void SyncBackendConnection::set_alert( const std::string& alert_name, + const std::string& host_name, + const std::string& user_name, + const std::string& topic_name, const backend::AlertKind& alert_kind, - double threshold) + double threshold, + const std::chrono::milliseconds& t_between_triggers, + const std::string& contact_info) { try { - StatisticsBackend::set_alert(alert_name, alert_kind, threshold); + StatisticsBackend::set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, t_between_triggers, contact_info); } catch (const Exception& e) { 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..c8fc3f43 100644 --- a/src/model/tree/TreeModel.cpp +++ b/src/model/tree/TreeModel.cpp @@ -282,4 +282,135 @@ 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 From 89ed3c45f6ebf27984cac77cd1f722163c16681d Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Mon, 29 Sep 2025 11:00:46 +0200 Subject: [PATCH 12/42] Adding new AlertList/Item models Signed-off-by: Emilio Cuesta --- CMakeLists.txt | 4 + .../fastdds_monitor/backend/backend_types.h | 6 +- .../fastdds_monitor/backend/backend_utils.h | 4 + .../model/alerts/AlertListItem.h | 224 +++++++++++++ .../model/alerts/AlertListModel.h | 207 ++++++++++++ src/backend/backend_utils.cpp | 19 +- src/model/alerts/AlertListItem.cpp | 154 +++++++++ src/model/alerts/AlertListModel.cpp | 304 ++++++++++++++++++ 8 files changed, 918 insertions(+), 4 deletions(-) create mode 100644 include/fastdds_monitor/model/alerts/AlertListItem.h create mode 100644 include/fastdds_monitor/model/alerts/AlertListModel.h create mode 100644 src/model/alerts/AlertListItem.cpp create mode 100644 src/model/alerts/AlertListModel.cpp 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/include/fastdds_monitor/backend/backend_types.h b/include/fastdds_monitor/backend/backend_types.h index c52cce0c..957a0a91 100644 --- a/include/fastdds_monitor/backend/backend_types.h +++ b/include/fastdds_monitor/backend/backend_types.h @@ -37,13 +37,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 AlertKind = eprosima::statistics_backend::AlertKind; +using AlertId = eprosima::statistics_backend::AlertId; using AlertInfo = eprosima::statistics_backend::AlertInfo; -using EntityInfo = eprosima::statistics_backend::Info; +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 6982f4f2..bf59354a 100644 --- a/include/fastdds_monitor/backend/backend_utils.h +++ b/include/fastdds_monitor/backend/backend_utils.h @@ -79,6 +79,10 @@ bool get_info_metatraffic( QString entity_kind_to_QString( const EntityKind& entity_kind); +//! 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); diff --git a/include/fastdds_monitor/model/alerts/AlertListItem.h b/include/fastdds_monitor/model/alerts/AlertListItem.h new file mode 100644 index 00000000..3e987b03 --- /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; + } + + /** + * @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..a5a6d43d --- /dev/null +++ b/include/fastdds_monitor/model/alerts/AlertListModel.h @@ -0,0 +1,207 @@ +// Copyright 2021 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/src/backend/backend_utils.cpp b/src/backend/backend_utils.cpp index eb4c5854..fdaed066 100644 --- a/src/backend/backend_utils.cpp +++ b/src/backend/backend_utils.cpp @@ -101,6 +101,21 @@ QString entity_kind_to_QString( } } +QString alert_kind_to_QString( + const AlertKind& alert_kind) +{ + switch (alert_kind) + { + case AlertKind::NEW_DATA: + return "New Data"; + case AlertKind::NO_DATA: + return "No Data"; + case AlertKind::INVALID: + default: + return "INVALID"; + } +} + std::string statistic_kind_to_string( const StatisticKind& statistic_kind) { @@ -309,7 +324,7 @@ AlertKind string_to_alert_kind( } else { - return AlertKind::NONE; + return AlertKind::INVALID; } } @@ -558,7 +573,7 @@ std::string entity_alert_description( case backend::AlertKind::NEW_DATA: return "New data on the entity has been received"; default: - case backend::AlertKind::NONE: + case backend::AlertKind::INVALID: return ""; } } diff --git a/src/model/alerts/AlertListItem.cpp b/src/model/alerts/AlertListItem.cpp new file mode 100644 index 00000000..c6bb60a8 --- /dev/null +++ b/src/model/alerts/AlertListItem.cpp @@ -0,0 +1,154 @@ +/**************************************************************************** +** +** Copyright (C) Paul Lemire, Tepee3DTeam and/or its subsidiary(-ies). +** Contact: paul.lemire@epitech.eu +** Contact: tepee3d_2014@labeip.epitech.eu +** +** This file is part of the Tepee3D project +** +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +****************************************************************************/ + +// 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_(backend::AlertId::invalid()) + , id_(-1) +{ +} + +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..2745b12f --- /dev/null +++ b/src/model/alerts/AlertListModel.cpp @@ -0,0 +1,304 @@ +/**************************************************************************** +** +** Copyright (C) Paul Lemire, Tepee3DTeam and/or its subsidiary(-ies). +** Contact: paul.lemire@epitech.eu +** Contact: tepee3d_2014@labeip.epitech.eu +** +** This file is part of the Tepee3D project +** +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +****************************************************************************/ + +// 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 From 33b8fd0f88cb9bcc5d086fbd5087f221339e7eb2 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 30 Sep 2025 15:55:12 +0200 Subject: [PATCH 13/42] Refs #23719: Alert List, Alert Data and Alert Messages are being correcly displayed Signed-off-by: Emilio Cuesta --- fastdds_monitor.pro | 2 + include/fastdds_monitor/Controller.h | 3 + include/fastdds_monitor/Engine.h | 62 +++-- .../backend/SyncBackendConnection.h | 35 +++ .../fastdds_monitor/backend/backend_utils.h | 8 + .../model/alerts/AlertListItem.h | 4 +- include/fastdds_monitor/model/model_types.h | 2 + mock/complex_mock/StatisticsBackend.cpp | 6 + qml.qrc | 1 + qml/AlertList.qml | 239 +++--------------- qml/AlertSummary.qml | 68 +++++ qml/AlertsPanel.qml | 53 ++-- qml/LeftPanel.qml | 12 +- qml/NewDataAlertDialog.qml | 70 ++--- qml/NoDataAlertDialog.qml | 188 ++++++++++---- qml/Panels.qml | 8 +- qml/main.qml | 4 +- src/Controller.cpp | 6 + src/Engine.cpp | 147 ++++++++--- src/backend/SyncBackendConnection.cpp | 120 +++++++++ src/backend/backend_utils.cpp | 37 +++ src/model/tree/TreeModel.cpp | 2 + 22 files changed, 678 insertions(+), 399 deletions(-) create mode 100644 qml/AlertSummary.qml 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 b4052cc9..4175b87a 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(); diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 91f6e0e3..51eae52d 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -338,6 +339,18 @@ 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, @@ -723,14 +736,29 @@ public slots: bool fill_status_(); /** - * @brief Clear and fill the Alert Model + * @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_(); + bool fill_alert_list_(); /** - * @brief Clear and fill the Alert Message Model + * @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 Clear and fill the aler messages view * * @return true if any change in any model has been done */ @@ -741,13 +769,13 @@ public slots: std::string callback, std::string time); - //! Add a new alert message to the Alert model - bool add_alert_info_( + //! Add a new alert object to the Alert model + bool add_alert_( std::string alert, std::string time); //! Add a new alert message to the Alert Message model -bool add_alert_message_info_( + bool add_alert_message_info_( std::string alert_name, std::string msg, std::string time); @@ -762,13 +790,6 @@ bool add_alert_message_info_( std::string name, std::string time); - /** - * Generates a new alert info model from the main schema - * The alert model schema has: - * - "Alerts" tag - to collect alerts - */ - void generate_new_alert_info_(); - /** * Generates a new alert message info model from the main schema * The Alert Message model schema has: @@ -865,9 +886,12 @@ bool add_alert_message_info_( //! Clear issues panel information void clear_issue_info_(); - //! Clear alerts panel information + //! Clear alerts info panel information void clear_alert_info_(); + //! Clear alert messages information + void clear_alert_message_info_(); + ///// // Variables @@ -899,9 +923,15 @@ bool add_alert_message_info_( backend::Info issue_info_; //! Data Model for Alerts. Collects alerts defined in the system - models::TreeModel* alert_model_; + models::AlertListModel* alert_model_; + + //! Alert buffer + backend::Info alert_data_; + + //! Data Model for Info of the clicked alert + models::TreeModel* alerts_summary_model_; - //! Data that is represented in the Alert Model when this model is refreshed + //! Information about the selected alert backend::Info alert_info_; //! Data Model for Alert Messages. Collects alert messages and info from the whole system diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 1b700288..ed118f1d 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 * ***********/ @@ -461,6 +474,20 @@ 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 + * @param inactive_visible Whether inactive alerts should be shown + * @param metatraffic_visible Whether metatraffic alerts should be shown + * @return true if any change has been made, false otherwise + */ + bool update_alerts_model( + models::AlertListModel* alerts_model, + bool inactive_visible, + bool metatraffic_visible); + ///// // Entity update functions @@ -642,6 +669,14 @@ class SyncBackendConnection bool metatraffic_visible, bool proxy_visible); + bool update_alert_item_( + AlertListItem* item, + bool inactive_visible, + bool metatraffic_visible); + + bool update_alert_item_info_( + AlertListItem* item); + /************** * UPDATE ONE * *************/ diff --git a/include/fastdds_monitor/backend/backend_utils.h b/include/fastdds_monitor/backend/backend_utils.h index bf59354a..f8cab724 100644 --- a/include/fastdds_monitor/backend/backend_utils.h +++ b/include/fastdds_monitor/backend/backend_utils.h @@ -79,6 +79,14 @@ 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); diff --git a/include/fastdds_monitor/model/alerts/AlertListItem.h b/include/fastdds_monitor/model/alerts/AlertListItem.h index 3e987b03..296dc8b5 100644 --- a/include/fastdds_monitor/model/alerts/AlertListItem.h +++ b/include/fastdds_monitor/model/alerts/AlertListItem.h @@ -19,8 +19,8 @@ * @file AlertListItem.hpp */ -#ifndef _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTListITEM_H -#define _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTListITEM_H +#ifndef _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTLISTITEM_H +#define _EPROSIMA_FASTDDS_MONITOR_MODEL_ALERTLISTITEM_H #include #include 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/mock/complex_mock/StatisticsBackend.cpp b/mock/complex_mock/StatisticsBackend.cpp index c8c093a7..2f95a20c 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(); +} + // Returns the EntityKind of the entity with id entity_id EntityKind StatisticsBackend::get_type( EntityId entity_id) diff --git a/qml.qrc b/qml.qrc index a10c1ce0..a94d0c89 100644 --- a/qml.qrc +++ b/qml.qrc @@ -19,6 +19,7 @@ qml/AlertKindDialog.qml qml/AlertList.qml qml/AlertsPanel.qml + qml/AlertSummary.qml qml/NewDataAlertDialog.qml qml/NoDataAlertDialog.qml qml/ChangeAliasDialog.qml diff --git a/qml/AlertList.qml b/qml/AlertList.qml index 549ba18b..10a45353 100644 --- a/qml/AlertList.qml +++ b/qml/AlertList.qml @@ -1,4 +1,4 @@ -// Copyright 2021 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). // // This file is part of eProsima Fast DDS Monitor. // @@ -24,16 +24,10 @@ import Theme 1.0 Rectangle { - id: alertList + id: alertListRect Layout.fillHeight: true Layout.fillWidth: true - enum DDSEntity { - Participant, - Endpoint, - Locator - } - property int verticalSpacing: 5 property int spacingIconLabel: 8 property int iconSize: 18 @@ -42,12 +36,11 @@ Rectangle { property int thirdIndentation: secondIndentation + iconSize + spacingIconLabel ListView { - id: participantList - model: participantModel - delegate: participantListDelegate + id: alertList + model: alertModel + delegate: alertListDelegate clip: true - width: parent.width - height: parent.height + anchors.fill : parent spacing: verticalSpacing boundsBehavior: Flickable.StopAtBounds @@ -57,208 +50,44 @@ Rectangle { } Component { - id: participantListDelegate + id: alertListDelegate Item { - id: participantItem - width: participantList.width - height: participantListColumn.childrenRect.height - - property var participantId: id - property int participantIdx: index - property var endpointList: endpointList - - Column { - id: participantListColumn + id: alertItem + width: alertListRect.width + height: alertHighlightRect.height + property var alertId: id + property int alertIdx: index - Rectangle { + Rectangle { - id: participantHighlightRect - width: alertList.width - height: participantItem.height - color: clicked ? Theme.eProsimaLightBlue : "transparent" + id: alertHighlightRect + width: alertList.width + height: alertIcon.height + color: clicked ? Theme.eProsimaLightBlue : "transparent" - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton - onDoubleClicked: { - if(endpointList.height === endpointList.collapseHeightFlag) { - endpointList.height = 0; - } else { - if (endpointList.childrenRect.height != 0) { - endpointList.height = endpointList.collapseHeightFlag; - } - } - } - onClicked: { - if(mouse.button & Qt.RightButton) { - openEntitiesMenu(controller.get_domain_id(id), id, name, kind, openMenuCaller.leftPanel) - } else { - controller.participant_click(id) - } - } + onClicked: { + controller.alert_click(id) } } - ListView { - id: endpointList - model: participantModel.subModelFromEntityId(participantId) - width: participantList.width - height: 0 - contentHeight: contentItem.childrenRect.height - clip: true - spacing: verticalSpacing - topMargin: verticalSpacing - delegate: endpointListDelegate - boundsBehavior: Flickable.StopAtBounds - - property int collapseHeightFlag: childrenRect.height + endpointList.topMargin - } - - Component { - id: endpointListDelegate - - Item { - id: endpointItem - height: endpointListColumn.childrenRect.height - - property var endpointId: id - property int endpointIdx: index - property var locatorList: locatorList - - ListView.onAdd: { - if(endpointList.height != 0) { - endpointList.height = endpointList.collapseHeightFlag; - } - } - - Column { - id: endpointListColumn - - Rectangle { - id: endpointHighlightRect - width: alertList.width - height: endpointIcon.height - color: clicked ? Theme.eProsimaLightBlue : "transparent" - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - - onDoubleClicked: { - if(locatorList.height === locatorList.collapseHeightFlag) { - locatorList.height = 0; - endpointList.height = - endpointList.height - locatorList.collapseHeightFlag; - } else { - if (locatorList.childrenRect.height != 0) { - locatorList.height = locatorList.collapseHeightFlag; - endpointList.height = endpointList.height + locatorList.height; - } - } - } - onClicked: { - if(mouse.button & Qt.RightButton) { - openEntitiesMenu(controller.get_domain_id(id), id, name, kind, openMenuCaller.leftPanel) - } else { - controller.endpoint_click(id) - } - } - } + RowLayout { + spacing: spacingIconLabel - RowLayout { - spacing: spacingIconLabel - - IconSVG { - id: endpointIcon - name: (kind == "DataReader") ? "datareader" : "datawriter" - size: iconSize - Layout.leftMargin: secondIndentation - color: entityLabelColor(clicked, alive) - } - Label { - text: name - color: entityLabelColor(clicked, alive) - } - } - } - - ListView { - id: locatorList - model: endpointList.model.subModelFromEntityId(endpointId) - width: participantList.width - height: 0 - contentHeight: contentItem.childrenRect.height - clip: true - delegate: locatorListDelegate - spacing: verticalSpacing - topMargin: verticalSpacing - boundsBehavior: Flickable.StopAtBounds - - property int collapseHeightFlag: childrenRect.height + locatorList.topMargin - } - - Component { - id: locatorListDelegate - - Item { - id: locatorItem - width: parent.width - height: locatorListColumn.childrenRect.height - - property int locatorIdx: index - - ListView.onAdd: { - if(locatorList.height != 0) { - var prevHeight = locatorList.height - locatorList.height = locatorList.collapseHeightFlag - endpointList.height = endpointList.height + locatorList.height - prevHeight - } - } - - Column { - id: locatorListColumn - - Rectangle { - id: locatorHighlightRect - width: alertList.width - height: locatorIcon.height - color: clicked ? Theme.eProsimaLightBlue : "transparent" - - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton | Qt.RightButton - - onClicked: { - if(mouse.button & Qt.RightButton) { - openEntitiesMenu(controller.get_domain_id(id), id, name, kind, openMenuCaller.leftPanel) - } else { - controller.locator_click(id) - } - } - } - - RowLayout { - spacing: spacingIconLabel - - IconSVG { - id: locatorIcon - name: "locator" - size: iconSize - Layout.leftMargin: thirdIndentation - color: entityLabelColor(clicked, alive) - } - Label { - text: name - color: entityLabelColor(clicked, alive) - } - } - } - } - } - } - } + IconSVG { + id: alertIcon + name: "alert" + size: iconSize + Layout.leftMargin: firstIndentation + color: entityLabelColor(clicked, alive) + } + Label { + text: name + color: entityLabelColor(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/AlertsPanel.qml b/qml/AlertsPanel.qml index e7d67ef5..77dda920 100644 --- a/qml/AlertsPanel.qml +++ b/qml/AlertsPanel.qml @@ -1,4 +1,4 @@ -// Copyright 2021 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). // // This file is part of eProsima Fast DDS Monitor. // @@ -22,7 +22,7 @@ import QtQml.Models 2.12 import Theme 1.0 /* - Sidebar containing the Status and the Log views. + Sidebar containing the Alerts */ ColumnLayout { id: alertsPanel @@ -81,7 +81,7 @@ ColumnLayout { Rectangle { id: alertListTitle Layout.fillWidth: true - height: infoTabBar.height + height: infoTabBar.height Label { text: "Alert List" anchors.verticalCenter: parent.verticalCenter @@ -105,49 +105,32 @@ ColumnLayout { } Item { - id: alertInfo + id: alertInfoLayout visible: true SplitView.fillHeight: true SplitView.preferredHeight: parent.height / 4 SplitView.minimumHeight: infoTabBar.height clip: true - Rectangle { - id: infoSelectedAlert - property string alert_id: "UNKNOWN_ALERT" - anchors.top: infoTabBar.bottom + TabBar { + id: infoTabBar + anchors.top: parent.top anchors.left: parent.left width: parent.width - height: infoTabBar.height - - TabBar { - id: infoTabBar - anchors.top: parent.top - anchors.left: parent.left - width: parent.width - TabButton { - text: qsTr("Info") - } + TabButton { + text: qsTr("Info") } + } - Rectangle - { - color: "transparent" - anchors.top: infoTabBar.bottom - height: infoTabBar.height - width: parent.width + StackLayout { + currentIndex: infoTabBar.currentIndex + anchors.top: infoTabBar.bottom + anchors.bottom: parent.bottom + anchors.left: parent.left + width: parent.width - Label { - id: infoSelectedEntityLabel - text: "No entity selected" - font.pointSize: 10 - font.italic: true - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } + AlertSummary { + id: alertSummaryView } } } diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index ae06fbf7..a65d0f7f 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -196,20 +196,20 @@ RowLayout { monitoringPanel.changeExplorerEntityInfo(status) } - function createAlert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers) { + function createAlert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info) { console.log("MOCK2: Creating alert for topic " + topicId + " with threshold " + threshold + " and time between triggers " + t_between_triggers) - controller.set_alert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers); + controller.set_alert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info); } - function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers) { + function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers, contact_info) { // TODO: Remove log console.log("MOCK: Creating no data alert for topic " + topicId + " with threshold " + threshold + " and time between triggers " + t_between_triggers) - createAlert(name, hostId, userId, topicId, "NO_DATA", threshold, t_between_triggers); + createAlert(name, hostId, userId, topicId, "NO_DATA", threshold, t_between_triggers, contact_info); } - function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers) { + function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers, contact_info) { // TODO: Remove log console.log("MOCK: Creating new data alert for topic " + topicId + " with time between triggers " + t_between_triggers) - createAlert(name, hostId, userId, topicId, "NEW_DATA", 0, t_between_triggers); + createAlert(name, hostId, userId, topicId, "NEW_DATA", 0, t_between_triggers, contact_info); } } diff --git a/qml/NewDataAlertDialog.qml b/qml/NewDataAlertDialog.qml index 7f991a47..d2d4d9cc 100644 --- a/qml/NewDataAlertDialog.qml +++ b/qml/NewDataAlertDialog.qml @@ -32,14 +32,14 @@ Dialog { property string currentHost: "" property string currentUser: "" property string currentTopic: "" - property int currentThreshold: 5 property int currentTimeBetweenAlerts: 5000 + property string contactInfo: "" x: (parent.width - width) / 2 y: (parent.height - height) / 2 signal createAlert(string alert_name, string host_name, string user_name, string topic_name, - int t_between_triggers) + int t_between_triggers, string contact_info) Component.onCompleted: { standardButton(Dialog.Ok).text = qsTrId("Add") @@ -55,14 +55,15 @@ Dialog { currentUser = userComboBox.currentText currentTopic = topicComboBox.currentText currentTimeBetweenAlerts = noDataTimeBetweenAlerts.value - createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentTimeBetweenAlerts) + //contactInfo = "" + createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentTimeBetweenAlerts, contactInfo) } onAboutToShow: { alertNameTextField.text = "" - hostComboBox.currentIndex = 0 - topicComboBox.currentIndex = 0 - userComboBox.currentIndex = 0 + hostComboBox.currentIndex = -1 + topicComboBox.currentIndex = -1 + userComboBox.currentIndex = -1 updateTopics() updateUsers() updateHosts() @@ -105,8 +106,9 @@ Dialog { AdaptiveComboBox { id: hostComboBox - textRole: "host" + textRole: "nameId" valueRole: "id" + popup.y: height displayText: currentIndex === -1 ? ("Please choose a host...") : currentText @@ -115,6 +117,7 @@ Dialog { onActivated: { activeOk = true + regenerateAlertName() } } @@ -129,8 +132,9 @@ Dialog { AdaptiveComboBox { id: userComboBox - textRole: "user" + textRole: "nameId" valueRole: "id" + popup.y: height displayText: currentIndex === -1 ? ("Please choose a user...") : currentText @@ -139,6 +143,7 @@ Dialog { onActivated: { activeOk = true + regenerateAlertName() } } @@ -153,8 +158,9 @@ Dialog { AdaptiveComboBox { id: topicComboBox - textRole: "topic" + textRole: "nameId" valueRole: "id" + popup.y: height displayText: currentIndex === -1 ? ("Please choose a topic...") : currentText @@ -163,25 +169,10 @@ Dialog { onActivated: { activeOk = true + regenerateAlertName() } } - Label { - text: "Threshold: " - InfoToolTip { - text: "Threshold of the throughput under which the alert will start triggering." - } - } - - SpinBox { - id: noDataThreshold - editable: true - from: 1 - to: 100 - stepSize: 1 - value: 5 - } - Label { text: "Time between alerts (ms): " InfoToolTip { @@ -196,6 +187,7 @@ Dialog { stepSize: 50 value: 5000 } + } MessageDialog { @@ -208,21 +200,7 @@ Dialog { onDiscard: newDataAlertDialog.close() } - MessageDialog { - id: emptyTopic - title: "Topic not selected" - icon: StandardIcon.Warning - standardButtons: StandardButton.Retry | StandardButton.Discard - text: "The topic field is empty. Please choose a topic from the list." - onAccepted: newDataAlertDialog.open() - onDiscard: newDataAlertDialog.close() - } - function checkInputs() { - if (topicComboBox.currentIndex === -1) { - emptyTopic.open() - return false - } if (alertNameTextField.text === "") { emptyAlertName.open() return false @@ -233,18 +211,26 @@ Dialog { 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] + "<" + entityName_id_str[entityName_id_str.length-1] } - function formatText(count, modelData) { - var data = count === 24 ? modelData + 1 : modelData; - return data.toString().length < 2 ? "0" + data : data; + function regenerateAlertName(){ + alertNameTextField.text = abbreviateEntityName(hostComboBox.currentText) + alertNameTextField.text += "_" + abbreviateEntityName(userComboBox.currentText) + alertNameTextField.text += "_" + abbreviateEntityName(topicComboBox.currentText) } } diff --git a/qml/NoDataAlertDialog.qml b/qml/NoDataAlertDialog.qml index cc1ff725..c6a1b7b5 100644 --- a/qml/NoDataAlertDialog.qml +++ b/qml/NoDataAlertDialog.qml @@ -27,36 +27,49 @@ Dialog { title: "Create new alert" standardButtons: Dialog.Ok | Dialog.Cancel + property bool activeOk: true + property string currentAlertName: "" + property string currentHost: "" + property string currentUser: "" + property string currentTopic: "" + property int currentThreshold: 5 + property int currentTimeBetweenAlerts: 5000 + property string contactInfo: "sample@email.com" + x: (parent.width - width) / 2 y: (parent.height - height) / 2 - property bool activeOk: true - signal createAlert(string alert_name, string host_name, string user_name, string topic_name, - int threshold, int t_between_triggers) + int t_between_triggers, int threshold, string contact_info) Component.onCompleted: { standardButton(Dialog.Ok).text = qsTrId("Add") standardButton(Dialog.Cancel).text = qsTrId("Close") - controller.update_available_entity_ids("Host", "getDataDialogSourceEntityId") - } - - onAboutToShow: { - getDataDialogSourceEntityId.currentIndex = 0 - updateAllEntities() - sourceEntityId.currentIndex = -1 - alertTextField.text = "" } onAccepted: { if (!checkInputs()) return + currentAlertName = alertNameTextField.text + currentHost = hostComboBox.currentText + currentUser = userComboBox.currentText currentTopic = topicComboBox.currentText - createAlert("fixed_name", "", "", currentTopic, noDataThreshold.value, 5000) + currentTimeBetweenAlerts = noDataTimeBetweenAlerts.value + currentThreshold = noDataThreshold.value + //contactInfo = "" + createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentTimeBetweenAlerts, currentThreshold, contactInfo) } - onClosed: activeOk = true + onAboutToShow: { + alertNameTextField.text = "" + hostComboBox.currentIndex = -1 + topicComboBox.currentIndex = -1 + userComboBox.currentIndex = -1 + updateTopics() + updateUsers() + updateHosts() + } GridLayout{ @@ -64,8 +77,8 @@ Dialog { rowSpacing: 20 Label { - id: alertLabel - text: "Alert label: " + id: alertNameLabel + text: "Alert name: " InfoToolTip { text: "Name of the alert.\n"+ "The alert name is autogerated\n" + @@ -73,8 +86,9 @@ Dialog { "dialog." } } + TextField { - id: alertTextField + id: alertNameTextField placeholderText: "" selectByMouse: true maximumLength: 100 @@ -83,56 +97,91 @@ Dialog { onTextEdited: activeOk = true } - Label { - id: entityKindLabel - text: "Entity kind: " + id: hostLabel + text: "Host: " InfoToolTip { - text: "Entity kind from which the data\n" + + text: "Host name from which the data\n" + "will be collected." } } - RowLayout { - AdaptiveComboBox { - id: getDataDialogSourceEntityId - model: [ - "Host", - "User", - "Process", - "Domain", - "Topic", - "DomainParticipant", - "DataWriter", - "DataReader", - "Locator"] + AdaptiveComboBox { + id: hostComboBox + textRole: "nameId" + valueRole: "id" + popup.y: height + displayText: currentIndex === -1 + ? ("Please choose a host...") + : currentText + model: alertHostModel + Component.onCompleted: currentIndex = -1 onActivated: { activeOk = true - updateEntities() + regenerateAlertName() } + } + + Label { + id: userLabel + text: "User: " + InfoToolTip { + text: "User name from which the data\n" + + "will be collected." } - AdaptiveComboBox { - id: sourceEntityId + } + + AdaptiveComboBox { + id: userComboBox textRole: "nameId" valueRole: "id" + popup.y: height displayText: currentIndex === -1 - ? ("Please choose a " + getDataDialogSourceEntityId.currentText + "...") + ? ("Please choose a user...") : currentText - model: entityModelFirst + model: alertUserModel + Component.onCompleted: currentIndex = -1 onActivated: { activeOk = true + regenerateAlertName() } + } + + Label { + id: topicLabel + text: "Topic: " + InfoToolTip { + text: "Topic name from which the data\n" + + "will be collected." } } + AdaptiveComboBox { + id: topicComboBox + textRole: "nameId" + valueRole: "id" + popup.y: height + displayText: currentIndex === -1 + ? ("Please choose a topic...") + : currentText + model: alertTopicModel + Component.onCompleted: currentIndex = -1 + + onActivated: { + activeOk = true + regenerateAlertName() + } + } + Label { text: "Threshold: " InfoToolTip { text: "Threshold of the throughput under which the alert will start triggering." } } + SpinBox { id: noDataThreshold editable: true @@ -141,38 +190,65 @@ Dialog { stepSize: 1 value: 5 } - } - MessageDialog { - id: emptyAlertLabel - title: "Missing alert label" - icon: StandardIcon.Warning - standardButtons: StandardButton.Retry | StandardButton.Discard - text: "The alert label field is empty. Please enter an alert label." - onAccepted: noDataAlertDialog.open() - onDiscard: noDataAlertDialog.close() + Label { + text: "Time between alerts (ms): " + InfoToolTip { + text: "Minimum time between two consecutive alerts." + } + } + SpinBox { + id: noDataTimeBetweenAlerts + editable: true + from: 0 + to: 10000 + stepSize: 50 + value: 5000 + } + } MessageDialog { - id: emptyEntityIdDialog - title: "Empty Entity Id" + id: emptyAlertName + title: "Missing alert name" icon: StandardIcon.Warning standardButtons: StandardButton.Retry | StandardButton.Discard - text: "The Entity Id field is empty. Please choose an Entity Id from the list." + text: "The alert name field is empty. Please enter an alert name." onAccepted: noDataAlertDialog.open() onDiscard: noDataAlertDialog.close() } function checkInputs() { - if (currentTopic.currentIndex === -1) { - emptyEntityIdDialog.open() - return false - } - if (alertTextField.text === "") { - emptyAlertLabel.open() + if (alertNameTextField.text === "") { + emptyAlertName.open() return false } return true } + + 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] + "<" + entityName_id_str[entityName_id_str.length-1] + } + + function regenerateAlertName(){ + alertNameTextField.text = abbreviateEntityName(hostComboBox.currentText) + alertNameTextField.text += "_" + abbreviateEntityName(userComboBox.currentText) + alertNameTextField.text += "_" + abbreviateEntityName(topicComboBox.currentText) + } } diff --git a/qml/Panels.qml b/qml/Panels.qml index 3f63dc2e..bdecad55 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -207,12 +207,12 @@ RowLayout { leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } - function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers){ - leftPanel.createNewDataAlert(name, hostId, userId, topicId, t_between_triggers) + function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers, contact_info){ + leftPanel.createNewDataAlert(name, hostId, userId, topicId, t_between_triggers, contact_info) } - function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers){ - leftPanel.createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers) + function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers, contact_info){ + leftPanel.createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers, contact_info) } } diff --git a/qml/main.qml b/qml/main.qml index 7c952b6b..80ef5878 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -131,7 +131,7 @@ ApplicationWindow { NewDataAlertDialog { id: newDataAlertDialog onCreateAlert: { - panels.createNewDataAlert(alert_name, host_name, user_name, topic_name, t_between_triggers) + panels.createNewDataAlert(alert_name, host_name, user_name, topic_name, t_between_triggers, contact_info) } } @@ -139,7 +139,7 @@ ApplicationWindow { NoDataAlertDialog { id: noDataAlertDialog onCreateAlert: { - panels.createNoDataAlert(alert_name, host_name, user_name, topic_name, threshold, t_between_triggers) + panels.createNoDataAlert(alert_name, host_name, user_name, topic_name, threshold, t_between_triggers, contact_info) } } diff --git a/src/Controller.cpp b/src/Controller.cpp index e608d8c7..3b779e40 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) diff --git a/src/Engine.cpp b/src/Engine.cpp index c3822c6d..8a47d977 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -37,6 +37,8 @@ #include #include #include +#include +#include #include #include #include @@ -92,9 +94,11 @@ QObject* Engine::enable() fill_log_(); // Creates a default json structure for alerts and fills the tree model with it - alert_model_ = new models::TreeModel(); - generate_new_alert_info_(); - fill_alert_(); + 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(); @@ -149,6 +153,7 @@ QObject* Engine::enable() 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_); @@ -260,6 +265,11 @@ Engine::~Engine() delete alert_model_; } + if (alerts_summary_model_) + { + delete alerts_summary_model_; + } + if (alert_message_model_) { delete alert_message_model_; @@ -453,9 +463,25 @@ bool Engine::fill_issue_() return true; } -bool Engine::fill_alert_() +bool Engine::fill_first_alert_summary_() { - alert_model_->update(alert_info_); + 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(); + std::cout << "Fill_alert_data called" << std::endl; + return backend_connection_.update_alerts_model(alert_model_, inactive_visible(), metatraffic_visible()); +} + +bool Engine::fill_alert_summary_( + backend::AlertId id /*ID_ALL*/) +{ + alerts_summary_model_->update(backend_connection_.get_info(id)); + std::cout << "Fill_alert_summary called with info " << backend_connection_.get_info(id).dump() << std::endl; return true; } @@ -477,15 +503,6 @@ bool Engine::fill_status_() return true; } -void Engine::generate_new_alert_info_() -{ - EntityInfo info; - - info["Alerts"] = EntityInfo(); - - alert_info_ = info; -} - void Engine::generate_new_alert_message_info_() { EntityInfo info; @@ -574,13 +591,11 @@ void Engine::clear_issue_info_() fill_issue_(); } -bool Engine::add_alert_info_( +bool Engine::add_alert_( std::string alert, std::string time) { - alert_info_["Alerts"][time] = alert; - fill_alert_(); - + fill_alert_list_(); return true; } @@ -590,16 +605,14 @@ bool Engine::add_alert_message_info_( std::string time) { alert_message_info_[alert_name][time] = msg; - fill_alert_message_(); - return true; } -void Engine::clear_alert_info_() +void Engine::clear_alert_message_info_() { - alert_info_ = EntityInfo(); - fill_alert_(); + alert_message_info_ = EntityInfo(); + fill_alert_message_(); } bool Engine::fill_first_entity_info_() @@ -844,6 +857,77 @@ bool Engine::entity_clicked( return res; } +bool Engine::alert_clicked( + backend::AlertId id) +{ + qDebug() << "Clicked alert: " ; + std::cout << "Clicked alert: " << std::endl; + + + // auto click_result = last_entities_clicked_.click(id, kind); + bool res = false; + + // if (std::get<2>(click_result).is_set()) + // { + // res = update_entity_generic( + // std::get<2>(click_result).id, + // std::get<2>(click_result).kind, + // true, + // false) || res; + // } + + // if (std::get<1>(click_result).is_set()) + // { + // res = update_entity_generic( + // std::get<1>(click_result).id, + // std::get<1>(click_result).kind, + // true, + // false) || res; + // } + + // switch (std::get<0>(click_result)) + // { + // case EntitiesClicked::EntityKindClicked::all: + // // Reset dds model and update if needed + // if (reset_dds) + // { + // reset_dds_data(); + // } + // if (update_dds) + // { + // res = update_dds_data(id) || res; + // } + // break; + + // case EntitiesClicked::EntityKindClicked::dds: + // res = update_entity_generic(id, kind, true, true) || res; + // break; + + // case EntitiesClicked::EntityKindClicked::logical_physical: + // // Update new entity + // res = update_entity_generic(id, kind, true, true) || res; + + // // Reset dds model and update if needed + // if (reset_dds) + // { + // reset_dds_data(); + // } + // if (update_dds) + // { + // res = update_dds_data(id) || res; + // } + // break; + + // default: + // break; + // } + + // All entities + res = fill_alert_summary_(id) || res; + + return res; +} + bool Engine::fill_available_entity_id_list_( backend::EntityKind entity_kind, QString entity_model_id) @@ -975,6 +1059,7 @@ void Engine::refresh_engine( fill_physical_data_(); fill_logical_data_(); + fill_alert_list_(); if (!maintain_clicked) { @@ -1252,7 +1337,7 @@ bool Engine::read_callback_( case backend::AlertKind::NO_DATA: return add_alert_message_info_(alert_callback.alert_info.get_alert_name(), "NO_DATA alert triggered", utils::now()); break; - case backend::AlertKind::NONE: + case backend::AlertKind::INVALID: default: // Unknown alerts are ignored break; @@ -1886,19 +1971,16 @@ void Engine::set_alert( const std::string& topic_name, const backend::AlertKind& alert_kind, double threshold, - const std::chrono::milliseconds& t_between_triggers) + const std::chrono::milliseconds& t_between_triggers, + const std::string& contact_info) { // Adding alert to backend structures - // backend_connection_.set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, t_between_triggers); - backend_connection_.set_alert(alert_name, "", "", "", alert_kind, threshold, t_between_triggers); + // backend_connection_.set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, t_between_triggers, contact_info); + backend_connection_.set_alert(alert_name, "", "", "", alert_kind, threshold, t_between_triggers, contact_info); std::cout << "Alert " << alert_name << " created with host " << host_name << ", user " << user_name << ", topic " << topic_name << ", threshold " << threshold << " and time between triggers " << t_between_triggers.count() << " ms" << std::endl; - - // Adding alert to engine and GUI structures - // NOTE: We cannot do this if we don't know the backend ID maybe - add_alert_info_(alert_name, utils::now()); } bool Engine::update_entity( @@ -1936,6 +2018,7 @@ void Engine::change_inactive_visible() fill_physical_data_(); fill_logical_data_(); fill_dds_data_(); + fill_alert_list_(); refresh_engine(); } @@ -1945,6 +2028,7 @@ void Engine::change_metatraffic_visible() fill_physical_data_(); fill_logical_data_(); fill_dds_data_(); + fill_alert_list_(); refresh_engine(); } @@ -1954,6 +2038,7 @@ void Engine::change_ros2_demangling() fill_physical_data_(); fill_logical_data_(); fill_dds_data_(); + fill_alert_list_(); refresh_engine(); } diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 2202f9f3..2a824468 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,88 @@ bool SyncBackendConnection::update_dds_model( proxy_visible); } +bool SyncBackendConnection::update_alert_item_( + AlertListItem* item, + bool inactive_visible, + bool metatraffic_visible) +{ + 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 inactive_visible, + bool metatraffic_visible) +{ + bool changed = false; + + // For each User get all processes + for (auto& alert_id : get_alerts()) + { + // AlertId alert_id = alert_tuple.first; + // AlertInfo alert_info = alert_tuple->second; + // 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) + { + // Only create the new alert if is alive or inactive are visible + if ((inactive_visible || get_alive(alert_id))) + { + // 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, inactive_visible, + metatraffic_visible) || changed; + + std::cout << alert_item->info().dump() << std::endl; + } + } + + // In case this entity is inactive and inactive are not being displayed + else if ((!inactive_visible && !get_alive(alert_id))) + { + models::AlertListItem* alert_item = alerts_model->at(index); + + // Remove the row + alerts_model->removeRow(index); + + // Remove its subentities and the object ListItem + delete alert_item; + + changed = true; + std::cout << alert_item->info().dump() << std::endl; + + } + + // Otherwise just update the entity + else + { + models::AlertListItem* alert_item = alerts_model->at(index); + changed = update_alert_item_(alert_item, inactive_visible, metatraffic_visible) + || changed; + std::cout << alert_item->info().dump() << std::endl; + + } + } + + return changed; +} + bool SyncBackendConnection::update_get_data_dialog_entity_id( models::ListModel* entity_model, EntityKind entity_kind, @@ -440,6 +529,7 @@ bool SyncBackendConnection::update_model_( return changed; } + bool SyncBackendConnection::set_listener( Listener* listener) { @@ -553,6 +643,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 +768,20 @@ 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) { diff --git a/src/backend/backend_utils.cpp b/src/backend/backend_utils.cpp index fdaed066..07ac925b 100644 --- a/src/backend/backend_utils.cpp +++ b/src/backend/backend_utils.cpp @@ -101,6 +101,43 @@ QString entity_kind_to_QString( } } +models::AlertId alert_backend_id_to_models_id( + const AlertId& id) +{ + std::ostringstream stream; + // if (id == ID_ALL) + // { + // stream << models::ID_ALL; + // } + // else if (id == ID_NONE) + // { + // stream << models::ID_INVALID; + // } + // else + { + stream << id; + } + return utils::to_QString(stream.str()); +} + +AlertId alert_models_id_to_backend_id( + const models::AlertId& id) +{ + std::ostringstream stream; + // if (id == models::ID_ALL) + // { + // return AlertId::all(); + // } + // else if (id == models::ID_INVALID || id == "") + // { + // return AlertId::invalid(); + // } + // else + { + return AlertId(id.toInt()); + } +} + QString alert_kind_to_QString( const AlertKind& alert_kind) { diff --git a/src/model/tree/TreeModel.cpp b/src/model/tree/TreeModel.cpp index c8fc3f43..150f71b4 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 From 0b58a7d3c9897c48a76180cee3578e7f275c9c02 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 30 Sep 2025 16:05:53 +0200 Subject: [PATCH 14/42] Refs #23719: Uncrustify and remove of unwanted logs and comments Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 16 ++-- include/fastdds_monitor/Engine.h | 24 +++--- include/fastdds_monitor/backend/Listener.h | 1 - .../backend/SyncBackendConnection.h | 14 ++-- include/fastdds_monitor/model/tree/TreeItem.h | 3 +- .../fastdds_monitor/model/tree/TreeModel.h | 15 ++-- src/Controller.cpp | 6 +- src/Engine.cpp | 82 ++----------------- src/backend/Listener.cpp | 5 +- src/backend/SyncBackendConnection.cpp | 29 +++---- src/model/alerts/AlertListItem.cpp | 2 +- src/model/tree/TreeModel.cpp | 31 ++++++- 12 files changed, 93 insertions(+), 135 deletions(-) diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 4175b87a..9cdf0d43 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -234,14 +234,14 @@ public slots: //! Adds a new alert void set_alert( - QString alert_name, - QString host_name, - QString user_name, - QString topic_name, - QString alert_type, - double threshold, - int time_between_triggers, - QString contact_info); + QString alert_name, + QString host_name, + QString user_name, + QString topic_name, + QString alert_type, + double threshold, + int time_between_triggers, + QString contact_info); //! Give a string with the name of the unit magnitud in which each DataKind is measured QString get_data_kind_units( diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 51eae52d..b63dea3a 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -499,14 +499,14 @@ class Engine : public QQmlApplicationEngine const backend::EntityKind& entity_kind); void set_alert( - const std::string& alert_name, - 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, - const std::string& contact_info); + const std::string& alert_name, + 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, + const std::string& contact_info); /** * This methods updates the info and summary if the entity clicked (the entity that is being shown) is the @@ -755,7 +755,7 @@ public slots: * @return true if any change in any model has been done */ bool fill_alert_summary_( - backend::AlertId id); + backend::AlertId id); /** * @brief Clear and fill the aler messages view @@ -776,9 +776,9 @@ public slots: //! 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); + std::string alert_name, + std::string msg, + std::string time); //! Add a new issue message to the Issue model bool add_issue_info_( diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index c827645c..91756cd9 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -104,7 +104,6 @@ class Listener : public PhysicalListener const AlertInfo& alert, const double& data) override; - protected: //! Engine reference diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index ed118f1d..796f5e66 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -484,9 +484,9 @@ class SyncBackendConnection * @return true if any change has been made, false otherwise */ bool update_alerts_model( - models::AlertListModel* alerts_model, - bool inactive_visible, - bool metatraffic_visible); + models::AlertListModel* alerts_model, + bool inactive_visible, + bool metatraffic_visible); ///// // Entity update functions @@ -670,12 +670,12 @@ class SyncBackendConnection bool proxy_visible); bool update_alert_item_( - AlertListItem* item, - bool inactive_visible, - bool metatraffic_visible); + AlertListItem* item, + bool inactive_visible, + bool metatraffic_visible); bool update_alert_item_info_( - AlertListItem* item); + AlertListItem* item); /************** * UPDATE ONE * diff --git a/include/fastdds_monitor/model/tree/TreeItem.h b/include/fastdds_monitor/model/tree/TreeItem.h index 7584e26e..e58c60d7 100644 --- a/include/fastdds_monitor/model/tree/TreeItem.h +++ b/include/fastdds_monitor/model/tree/TreeItem.h @@ -64,7 +64,8 @@ class TreeItem int row); - void remove_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 66fcdbba..7f5e48d8 100644 --- a/include/fastdds_monitor/model/tree/TreeModel.h +++ b/include/fastdds_monitor/model/tree/TreeModel.h @@ -112,7 +112,8 @@ class TreeModel : public QAbstractItemModel json data); //! Clear the model and create a new tree with new data without collapsing the view - void update_without_collapse(json& data); + 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; @@ -149,7 +150,7 @@ 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 * @@ -157,7 +158,10 @@ class TreeModel : public QAbstractItemModel * @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); + void setup_model_data_without_collapse( + TreeItem* parent, + const QModelIndex& parent_index, + const json& json_data); /** * @brief Iterates over the children of a node to find one with a specific name @@ -165,8 +169,9 @@ class TreeModel : public QAbstractItemModel * @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; - + TreeItem* find_child_by_name( + TreeItem* parent, + const QString& name) const; private: diff --git a/src/Controller.cpp b/src/Controller.cpp index 3b779e40..ef65d8da 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -301,10 +301,12 @@ void Controller::set_alias( backend::string_to_entity_kind(entity_kind)); } -std::string clean_entity_name(std::string original_name) +std::string clean_entity_name( + std::string original_name) { size_t pos = original_name.find(':'); - if (pos != std::string::npos) { + if (pos != std::string::npos) + { return original_name.substr(0, pos); } return original_name; diff --git a/src/Engine.cpp b/src/Engine.cpp index 8a47d977..b8fe45e8 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -473,7 +473,6 @@ bool Engine::fill_first_alert_summary_() bool Engine::fill_alert_list_() { alert_model_->clear(); - std::cout << "Fill_alert_data called" << std::endl; return backend_connection_.update_alerts_model(alert_model_, inactive_visible(), metatraffic_visible()); } @@ -481,7 +480,6 @@ bool Engine::fill_alert_summary_( backend::AlertId id /*ID_ALL*/) { alerts_summary_model_->update(backend_connection_.get_info(id)); - std::cout << "Fill_alert_summary called with info " << backend_connection_.get_info(id).dump() << std::endl; return true; } @@ -860,71 +858,9 @@ bool Engine::entity_clicked( bool Engine::alert_clicked( backend::AlertId id) { - qDebug() << "Clicked alert: " ; - std::cout << "Clicked alert: " << std::endl; - - - // auto click_result = last_entities_clicked_.click(id, kind); + qDebug() << "Clicked alert: "; bool res = false; - - // if (std::get<2>(click_result).is_set()) - // { - // res = update_entity_generic( - // std::get<2>(click_result).id, - // std::get<2>(click_result).kind, - // true, - // false) || res; - // } - - // if (std::get<1>(click_result).is_set()) - // { - // res = update_entity_generic( - // std::get<1>(click_result).id, - // std::get<1>(click_result).kind, - // true, - // false) || res; - // } - - // switch (std::get<0>(click_result)) - // { - // case EntitiesClicked::EntityKindClicked::all: - // // Reset dds model and update if needed - // if (reset_dds) - // { - // reset_dds_data(); - // } - // if (update_dds) - // { - // res = update_dds_data(id) || res; - // } - // break; - - // case EntitiesClicked::EntityKindClicked::dds: - // res = update_entity_generic(id, kind, true, true) || res; - // break; - - // case EntitiesClicked::EntityKindClicked::logical_physical: - // // Update new entity - // res = update_entity_generic(id, kind, true, true) || res; - - // // Reset dds model and update if needed - // if (reset_dds) - // { - // reset_dds_data(); - // } - // if (update_dds) - // { - // res = update_dds_data(id) || res; - // } - // break; - - // default: - // break; - // } - - // All entities res = fill_alert_summary_(id) || res; - return res; } @@ -1325,17 +1261,19 @@ bool Engine::read_callback_( 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_); // Add callback to log model switch (alert_callback.alert_info.get_alert_kind()) { case backend::AlertKind::NEW_DATA: - return add_alert_message_info_(alert_callback.alert_info.get_alert_name(), "NEW_DATA alert triggered", utils::now()); + return add_alert_message_info_( + alert_callback.alert_info.get_alert_name(), "NEW_DATA alert triggered", utils::now()); break; case backend::AlertKind::NO_DATA: - return add_alert_message_info_(alert_callback.alert_info.get_alert_name(), "NO_DATA alert triggered", utils::now()); + return add_alert_message_info_( + alert_callback.alert_info.get_alert_name(), "NO_DATA alert triggered", utils::now()); break; case backend::AlertKind::INVALID: default: @@ -1975,12 +1913,8 @@ void Engine::set_alert( const std::string& contact_info) { // Adding alert to backend structures - // backend_connection_.set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, t_between_triggers, contact_info); - backend_connection_.set_alert(alert_name, "", "", "", alert_kind, threshold, t_between_triggers, contact_info); - - std::cout << "Alert " << alert_name << " created with host " << host_name << ", user " << user_name - << ", topic " << topic_name - << ", threshold " << threshold << " and time between triggers " << t_between_triggers.count() << " ms" << std::endl; + backend_connection_.set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, + t_between_triggers, contact_info); } bool Engine::update_entity( diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index 03f6c4c0..dad45dfa 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -180,11 +180,10 @@ void Listener::on_status_reported( void Listener::on_alert_triggered( EntityId domain_id, EntityId entity_id, - const AlertInfo &alert, - const double &data) + const AlertInfo& alert, + const double& data) { engine_->add_callback(AlertCallback(domain_id, entity_id, alert, data)); } - } //namespace backend diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 2a824468..7830a513 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -380,8 +380,6 @@ bool SyncBackendConnection::update_alerts_model( changed = update_alert_item_(alert_item, inactive_visible, metatraffic_visible) || changed; - - std::cout << alert_item->info().dump() << std::endl; } } @@ -397,8 +395,6 @@ bool SyncBackendConnection::update_alerts_model( delete alert_item; changed = true; - std::cout << alert_item->info().dump() << std::endl; - } // Otherwise just update the entity @@ -407,8 +403,6 @@ bool SyncBackendConnection::update_alerts_model( models::AlertListItem* alert_item = alerts_model->at(index); changed = update_alert_item_(alert_item, inactive_visible, metatraffic_visible) || changed; - std::cout << alert_item->info().dump() << std::endl; - } } @@ -529,7 +523,6 @@ bool SyncBackendConnection::update_model_( return changed; } - bool SyncBackendConnection::set_listener( Listener* listener) { @@ -768,7 +761,8 @@ std::vector SyncBackendConnection::get_entities( } } -std::vector SyncBackendConnection::get_alerts(){ +std::vector SyncBackendConnection::get_alerts() +{ try { return StatisticsBackend::get_alerts(); @@ -1343,18 +1337,19 @@ void SyncBackendConnection::set_alias( } void SyncBackendConnection::set_alert( - const std::string& alert_name, - 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, - const std::string& contact_info) + const std::string& alert_name, + 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, + const std::string& contact_info) { try { - StatisticsBackend::set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, t_between_triggers, contact_info); + StatisticsBackend::set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, + t_between_triggers, contact_info); } catch (const Exception& e) { diff --git a/src/model/alerts/AlertListItem.cpp b/src/model/alerts/AlertListItem.cpp index c6bb60a8..15b357c8 100644 --- a/src/model/alerts/AlertListItem.cpp +++ b/src/model/alerts/AlertListItem.cpp @@ -56,7 +56,7 @@ AlertListItem::AlertListItem( QObject* parent) : QObject(parent) // , id_(backend::AlertId::invalid()) - , id_(-1) + , id_(-1) { } diff --git a/src/model/tree/TreeModel.cpp b/src/model/tree/TreeModel.cpp index 150f71b4..c84e72c2 100644 --- a/src/model/tree/TreeModel.cpp +++ b/src/model/tree/TreeModel.cpp @@ -284,19 +284,25 @@ void TreeModel::update( emit updatedData(); } -TreeItem* TreeModel::find_child_by_name(TreeItem* parent, const QString& name) const +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) +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) @@ -322,13 +328,21 @@ void TreeModel::setup_model_data_without_collapse(TreeItem* parent, const QModel { 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) @@ -363,13 +377,21 @@ void TreeModel::setup_model_data_without_collapse(TreeItem* parent, const QModel 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 { @@ -407,7 +429,8 @@ void TreeModel::setup_model_data_without_collapse(TreeItem* parent, const QModel } } -void TreeModel::update_without_collapse(json& data) +void TreeModel::update_without_collapse( + json& data) { std::unique_lock lock(update_mutex_); // Recursive function to update without collapsing From 0746651331db817d3a674f03e7acbd048b1412cc Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 1 Oct 2025 10:56:01 +0200 Subject: [PATCH 15/42] Removing unwanted logs Signed-off-by: Emilio Cuesta --- qml/LeftPanel.qml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index a65d0f7f..a97a1529 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -202,14 +202,10 @@ RowLayout { } function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers, contact_info) { - // TODO: Remove log - console.log("MOCK: Creating no data alert for topic " + topicId + " with threshold " + threshold + " and time between triggers " + t_between_triggers) createAlert(name, hostId, userId, topicId, "NO_DATA", threshold, t_between_triggers, contact_info); } function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers, contact_info) { - // TODO: Remove log - console.log("MOCK: Creating new data alert for topic " + topicId + " with time between triggers " + t_between_triggers) createAlert(name, hostId, userId, topicId, "NEW_DATA", 0, t_between_triggers, contact_info); } } From c2c0b1ec572c9b7c182ddf7931545d42795bdaf6 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 1 Oct 2025 18:53:16 +0200 Subject: [PATCH 16/42] Remove log Signed-off-by: Emilio Cuesta --- qml/LeftPanel.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index a97a1529..31525b79 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -197,7 +197,6 @@ RowLayout { } function createAlert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info) { - console.log("MOCK2: Creating alert for topic " + topicId + " with threshold " + threshold + " and time between triggers " + t_between_triggers) controller.set_alert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info); } From d41d6cfd5a47762de78cf29d3fe074deb20addee Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Thu, 2 Oct 2025 10:37:15 +0200 Subject: [PATCH 17/42] Fixing compilation errors after backend rebase Signed-off-by: Emilio Cuesta --- src/Engine.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Engine.cpp b/src/Engine.cpp index b8fe45e8..fd978a1b 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -902,7 +902,8 @@ bool Engine::on_selected_entity_kind( alert_host_id_model_, backend::EntityKind::HOST, inactive_visible(), - metatraffic_visible()); + metatraffic_visible(), + proxy_visible()); } else if (entity_model_id == "alertUser") { @@ -911,7 +912,8 @@ bool Engine::on_selected_entity_kind( alert_user_id_model_, backend::EntityKind::USER, inactive_visible(), - metatraffic_visible()); + metatraffic_visible(), + proxy_visible()); } else if (entity_model_id == "alertTopic") { @@ -920,7 +922,8 @@ bool Engine::on_selected_entity_kind( alert_topic_id_model_, backend::EntityKind::TOPIC, inactive_visible(), - metatraffic_visible()); + metatraffic_visible(), + proxy_visible()); } else { From 8484f4694d8abc0726f8bae4a58a594060ce1389 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Mon, 6 Oct 2025 14:36:28 +0200 Subject: [PATCH 18/42] Simplifying add_alert menu Signed-off-by: Emilio Cuesta --- qml.qrc | 4 +- qml/AlertDialog.qml | 508 +++++++++++++++++++++++++++++++++++++ qml/AlertKindDialog.qml | 94 ------- qml/AlertsPanel.qml | 2 +- qml/LeftPanel.qml | 8 - qml/MonitorMenuBar.qml | 2 +- qml/MonitorToolBar.qml | 2 +- qml/NewDataAlertDialog.qml | 236 ----------------- qml/NoDataAlertDialog.qml | 254 ------------------- qml/Panels.qml | 9 +- qml/TopicMenu.qml | 2 +- qml/main.qml | 23 +- 12 files changed, 518 insertions(+), 626 deletions(-) create mode 100644 qml/AlertDialog.qml delete mode 100644 qml/AlertKindDialog.qml delete mode 100644 qml/NewDataAlertDialog.qml delete mode 100644 qml/NoDataAlertDialog.qml diff --git a/qml.qrc b/qml.qrc index a94d0c89..c4c83aa0 100644 --- a/qml.qrc +++ b/qml.qrc @@ -16,12 +16,10 @@ qml/AboutDialog.qml qml/AdaptiveComboBox.qml qml/AdaptiveMenu.qml - qml/AlertKindDialog.qml + qml/AlertDialog.qml qml/AlertList.qml qml/AlertsPanel.qml qml/AlertSummary.qml - qml/NewDataAlertDialog.qml - qml/NoDataAlertDialog.qml qml/ChangeAliasDialog.qml qml/ChartsLayout.qml qml/CustomLegend.qml diff --git a/qml/AlertDialog.qml b/qml/AlertDialog.qml new file mode 100644 index 00000000..cbbfd55b --- /dev/null +++ b/qml/AlertDialog.qml @@ -0,0 +1,508 @@ +// 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 currentHost: "" + property string currentUser: "" + property string currentTopic: "" + property int currentThreshold: 5 + property int currentTimeBetweenAlerts: 5000 + property string contactInfo: "sample@email.com" + + modal: false + title: "Create new alert" + standardButtons: Dialog.Ok | Dialog.Cancel + + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + + signal createAlert(string alert_name, string host_name, string user_name, string topic_name, + string alert_type, int t_between_triggers, int threshold, string contact_info) + + 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 + 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 = parseInt(alertThreshold.value) + createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentKind, currentTimeBetweenAlerts, currentThreshold, contactInfo) + } + + onAboutToShow: { + alertKindComboBox.currentIndex = -1 + alertNameTextField.text = "" + hostComboBox.currentIndex = -1 + topicComboBox.currentIndex = -1 + userComboBox.currentIndex = -1 + 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: hostLabel + text: "Host: " + InfoToolTip { + text: "Host name from which the data\n" + + "will be collected." + } + } + + 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 name from which the data\n" + + "will be collected." + } + } + + 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 name from which the data\n" + + "will be collected." + } + } + + 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 + } + } + } + } + + Column { + width: parent.width + visible: advancedOptionsSubmenu.isExpanded + height: advancedOptionsSubmenu.isExpanded ? advancedOptionsSubmenu.item_height_ : 0 + spacing: alertDialog.layout_horizontal_spacing_ + + Row { + id: manualHost + spacing: alertDialog.layout_vertical_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 + height: 5/6*advancedOptionsSubmenu.item_height_ + anchors.verticalCenter: parent.verticalCenter + + background: Rectangle { + color: !manualHostCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } + + onTextChanged: { + } + } + } + + Row { + id: manualUser + spacing: alertDialog.layout_vertical_spacing_ + 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 + height: 5/6*advancedOptionsSubmenu.item_height_ + anchors.verticalCenter: parent.verticalCenter + + background: Rectangle { + color: !manualUserCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } + + onTextChanged: { + } + } + } + + Row { + id: manualTopic + spacing: alertDialog.layout_vertical_spacing_ + 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 + height: 5/6*advancedOptionsSubmenu.item_height_ + anchors.verticalCenter: parent.verticalCenter + + background: Rectangle { + color: !manualTopicCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } + + onTextChanged: { + } + } + } + } + } + } + } + + 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() + } + + function checkInputs() { + if (alertNameTextField.text === "") { + emptyAlertName.open() + return false + } + + return true + } + + 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/AlertKindDialog.qml b/qml/AlertKindDialog.qml deleted file mode 100644 index e7c8d645..00000000 --- a/qml/AlertKindDialog.qml +++ /dev/null @@ -1,94 +0,0 @@ -// 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.0 -import QtQuick.Dialogs 1.2 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.3 -import Theme 1.0 - -Dialog { - id: alertKindDialog - modal: false - title: "Create new alert" - standardButtons: Dialog.Ok | Dialog.Cancel - - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - - property var availableAlertKinds: [] - - signal createAlert(string alertKind) - - onAccepted: { - if (!checkInputs()) - return - createAlert(alertKindComboBox.currentText) - } - - Component.onCompleted: { - availableAlertKinds = controller.get_alert_kinds() - } - - onAboutToShow: { - alertKindComboBox.currentIndex = -1 - } - - 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 - } - - } - - MessageDialog { - id: emptyAlertKind - title: "Alert Kind" - icon: StandardIcon.Warning - standardButtons: StandardButton.Retry | StandardButton.Discard - text: "The alert kind field is empty. Please choose an alert kind from the list." - onAccepted: alertKindDialog.open() - onDiscard: alertKindDialog.close() - } - - function checkInputs() { - if (alertKindComboBox.currentIndex === -1) { - emptyAlertKind.open() - return false - } - - return true - } - -} diff --git a/qml/AlertsPanel.qml b/qml/AlertsPanel.qml index 77dda920..b6039ae5 100644 --- a/qml/AlertsPanel.qml +++ b/qml/AlertsPanel.qml @@ -55,7 +55,7 @@ ColumnLayout { anchors.fill: parent onClicked: { - alertKindDialog.open() + alertDialog.open() } } } diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index 31525b79..84c61a74 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -199,12 +199,4 @@ RowLayout { function createAlert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info) { controller.set_alert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info); } - - function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers, contact_info) { - createAlert(name, hostId, userId, topicId, "NO_DATA", threshold, t_between_triggers, contact_info); - } - - function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers, contact_info) { - createAlert(name, hostId, userId, topicId, "NEW_DATA", 0, t_between_triggers, contact_info); - } } diff --git a/qml/MonitorMenuBar.qml b/qml/MonitorMenuBar.qml index 0b270f91..c9ae1d64 100644 --- a/qml/MonitorMenuBar.qml +++ b/qml/MonitorMenuBar.qml @@ -77,7 +77,7 @@ MenuBar { } Action { text: qsTr("Create Alert") - onTriggered: alertKindDialog.open() + onTriggered: alertDialog.open() } MenuSeparator { } Action { diff --git a/qml/MonitorToolBar.qml b/qml/MonitorToolBar.qml index 34015000..e6b2fa6a 100644 --- a/qml/MonitorToolBar.qml +++ b/qml/MonitorToolBar.qml @@ -82,7 +82,7 @@ ToolBar { iconName: "alert" tooltipText: "Create alert" visible: isVisibleCreateAlert - onClicked: alertKindDialog.open() + onClicked: alertDialog.open() } MonitorToolBarButton { diff --git a/qml/NewDataAlertDialog.qml b/qml/NewDataAlertDialog.qml deleted file mode 100644 index d2d4d9cc..00000000 --- a/qml/NewDataAlertDialog.qml +++ /dev/null @@ -1,236 +0,0 @@ -// 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.0 -import QtQuick.Dialogs 1.2 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.3 -import Theme 1.0 - -Dialog { - id: newDataAlertDialog - modal: false - title: "Create new alert" - standardButtons: Dialog.Ok | Dialog.Cancel - - property bool activeOk: true - property string currentAlertName: "" - property string currentHost: "" - property string currentUser: "" - property string currentTopic: "" - property int currentTimeBetweenAlerts: 5000 - property string contactInfo: "" - - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - - signal createAlert(string alert_name, string host_name, string user_name, string topic_name, - int t_between_triggers, string contact_info) - - Component.onCompleted: { - standardButton(Dialog.Ok).text = qsTrId("Add") - standardButton(Dialog.Cancel).text = qsTrId("Close") - } - - onAccepted: { - if (!checkInputs()) - return - - currentAlertName = alertNameTextField.text - currentHost = hostComboBox.currentText - currentUser = userComboBox.currentText - currentTopic = topicComboBox.currentText - currentTimeBetweenAlerts = noDataTimeBetweenAlerts.value - //contactInfo = "" - createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentTimeBetweenAlerts, contactInfo) - } - - onAboutToShow: { - alertNameTextField.text = "" - hostComboBox.currentIndex = -1 - topicComboBox.currentIndex = -1 - userComboBox.currentIndex = -1 - updateTopics() - updateUsers() - updateHosts() - } - - GridLayout{ - - columns: 2 - rowSpacing: 20 - - 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: hostLabel - text: "Host: " - InfoToolTip { - text: "Host name from which the data\n" + - "will be collected." - } - } - - AdaptiveComboBox { - id: hostComboBox - textRole: "nameId" - valueRole: "id" - popup.y: height - displayText: currentIndex === -1 - ? ("Please choose a host...") - : currentText - model: alertHostModel - Component.onCompleted: currentIndex = -1 - - onActivated: { - activeOk = true - regenerateAlertName() - } - } - - Label { - id: userLabel - text: "User: " - InfoToolTip { - text: "User name from which the data\n" + - "will be collected." - } - } - - AdaptiveComboBox { - id: userComboBox - textRole: "nameId" - valueRole: "id" - popup.y: height - displayText: currentIndex === -1 - ? ("Please choose a user...") - : currentText - model: alertUserModel - Component.onCompleted: currentIndex = -1 - - onActivated: { - activeOk = true - regenerateAlertName() - } - } - - Label { - id: topicLabel - text: "Topic: " - InfoToolTip { - text: "Topic name from which the data\n" + - "will be collected." - } - } - - AdaptiveComboBox { - id: topicComboBox - textRole: "nameId" - valueRole: "id" - popup.y: height - displayText: currentIndex === -1 - ? ("Please choose a topic...") - : currentText - model: alertTopicModel - Component.onCompleted: currentIndex = -1 - - onActivated: { - activeOk = true - regenerateAlertName() - } - } - - Label { - text: "Time between alerts (ms): " - InfoToolTip { - text: "Minimum time between two consecutive alerts." - } - } - SpinBox { - id: noDataTimeBetweenAlerts - editable: true - from: 0 - to: 10000 - stepSize: 50 - value: 5000 - } - - } - - 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: newDataAlertDialog.open() - onDiscard: newDataAlertDialog.close() - } - - function checkInputs() { - if (alertNameTextField.text === "") { - emptyAlertName.open() - return false - } - - return true - } - - 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] + "<" + entityName_id_str[entityName_id_str.length-1] - } - - function regenerateAlertName(){ - alertNameTextField.text = abbreviateEntityName(hostComboBox.currentText) - alertNameTextField.text += "_" + abbreviateEntityName(userComboBox.currentText) - alertNameTextField.text += "_" + abbreviateEntityName(topicComboBox.currentText) - } -} diff --git a/qml/NoDataAlertDialog.qml b/qml/NoDataAlertDialog.qml deleted file mode 100644 index c6a1b7b5..00000000 --- a/qml/NoDataAlertDialog.qml +++ /dev/null @@ -1,254 +0,0 @@ -// 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.0 -import QtQuick.Dialogs 1.2 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.3 -import Theme 1.0 - -Dialog { - id: noDataAlertDialog - modal: false - title: "Create new alert" - standardButtons: Dialog.Ok | Dialog.Cancel - - property bool activeOk: true - property string currentAlertName: "" - property string currentHost: "" - property string currentUser: "" - property string currentTopic: "" - property int currentThreshold: 5 - property int currentTimeBetweenAlerts: 5000 - property string contactInfo: "sample@email.com" - - x: (parent.width - width) / 2 - y: (parent.height - height) / 2 - - signal createAlert(string alert_name, string host_name, string user_name, string topic_name, - int t_between_triggers, int threshold, string contact_info) - - Component.onCompleted: { - standardButton(Dialog.Ok).text = qsTrId("Add") - standardButton(Dialog.Cancel).text = qsTrId("Close") - } - - onAccepted: { - if (!checkInputs()) - return - - currentAlertName = alertNameTextField.text - currentHost = hostComboBox.currentText - currentUser = userComboBox.currentText - currentTopic = topicComboBox.currentText - currentTimeBetweenAlerts = noDataTimeBetweenAlerts.value - currentThreshold = noDataThreshold.value - //contactInfo = "" - createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentTimeBetweenAlerts, currentThreshold, contactInfo) - } - - onAboutToShow: { - alertNameTextField.text = "" - hostComboBox.currentIndex = -1 - topicComboBox.currentIndex = -1 - userComboBox.currentIndex = -1 - updateTopics() - updateUsers() - updateHosts() - } - - GridLayout{ - - columns: 2 - rowSpacing: 20 - - 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: hostLabel - text: "Host: " - InfoToolTip { - text: "Host name from which the data\n" + - "will be collected." - } - } - - AdaptiveComboBox { - id: hostComboBox - textRole: "nameId" - valueRole: "id" - popup.y: height - displayText: currentIndex === -1 - ? ("Please choose a host...") - : currentText - model: alertHostModel - Component.onCompleted: currentIndex = -1 - - onActivated: { - activeOk = true - regenerateAlertName() - } - } - - Label { - id: userLabel - text: "User: " - InfoToolTip { - text: "User name from which the data\n" + - "will be collected." - } - } - - AdaptiveComboBox { - id: userComboBox - textRole: "nameId" - valueRole: "id" - popup.y: height - displayText: currentIndex === -1 - ? ("Please choose a user...") - : currentText - model: alertUserModel - Component.onCompleted: currentIndex = -1 - - onActivated: { - activeOk = true - regenerateAlertName() - } - } - - Label { - id: topicLabel - text: "Topic: " - InfoToolTip { - text: "Topic name from which the data\n" + - "will be collected." - } - } - - AdaptiveComboBox { - id: topicComboBox - textRole: "nameId" - valueRole: "id" - popup.y: height - displayText: currentIndex === -1 - ? ("Please choose a topic...") - : currentText - model: alertTopicModel - Component.onCompleted: currentIndex = -1 - - onActivated: { - activeOk = true - regenerateAlertName() - } - } - - Label { - text: "Threshold: " - InfoToolTip { - text: "Threshold of the throughput under which the alert will start triggering." - } - } - - SpinBox { - id: noDataThreshold - editable: true - from: 1 - to: 100 - stepSize: 1 - value: 5 - } - - Label { - text: "Time between alerts (ms): " - InfoToolTip { - text: "Minimum time between two consecutive alerts." - } - } - SpinBox { - id: noDataTimeBetweenAlerts - editable: true - from: 0 - to: 10000 - stepSize: 50 - value: 5000 - } - - } - - 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: noDataAlertDialog.open() - onDiscard: noDataAlertDialog.close() - } - - function checkInputs() { - if (alertNameTextField.text === "") { - emptyAlertName.open() - return false - } - - return true - } - - 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] + "<" + entityName_id_str[entityName_id_str.length-1] - } - - function regenerateAlertName(){ - alertNameTextField.text = abbreviateEntityName(hostComboBox.currentText) - alertNameTextField.text += "_" + abbreviateEntityName(userComboBox.currentText) - alertNameTextField.text += "_" + abbreviateEntityName(topicComboBox.currentText) - } -} diff --git a/qml/Panels.qml b/qml/Panels.qml index bdecad55..414b750a 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -207,12 +207,7 @@ RowLayout { leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } - function createNewDataAlert(name, hostId, userId, topicId, t_between_triggers, contact_info){ - leftPanel.createNewDataAlert(name, hostId, userId, topicId, t_between_triggers, contact_info) + function createAlert(name, hostId, userId, topicId, alert_type, t_between_triggers, contact_info){ + leftPanel.createAlert(name, hostId, userId, topicId, alert_type, t_between_triggers, contact_info) } - - function createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers, contact_info){ - leftPanel.createNoDataAlert(name, hostId, userId, topicId, threshold, t_between_triggers, contact_info) - } - } diff --git a/qml/TopicMenu.qml b/qml/TopicMenu.qml index 8a1f23cc..f23460cb 100644 --- a/qml/TopicMenu.qml +++ b/qml/TopicMenu.qml @@ -48,7 +48,7 @@ Menu { onTriggered: openIDLView(menu.entityId) } MenuItem { - text: "Set New Data Alarm" + text: "Set New Data Alert" onTriggered: { newDataAlertDialog.currentTopic = menu.entityId newDataAlertDialog.open() diff --git a/qml/main.qml b/qml/main.qml index 80ef5878..f8b76dda 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -120,30 +120,13 @@ ApplicationWindow { onCreateChart: panels.createDynamicChart(dataKind, timeWindowSeconds, updatePeriod, maxPoints) } - AlertKindDialog { - id: alertKindDialog + AlertDialog { + id: alertDialog onCreateAlert: { - if (alertKind === "NEW_DATA") newDataAlertDialog.open() - else if (alertKind === "NO_DATA") noDataAlertDialog.open() + panels.createAlert(alert_name, host_name, user_name, topic_name, alert_type, threshold, t_between_triggers, contact_info) } } - NewDataAlertDialog { - id: newDataAlertDialog - onCreateAlert: { - panels.createNewDataAlert(alert_name, host_name, user_name, topic_name, t_between_triggers, contact_info) - } - } - - - NoDataAlertDialog { - id: noDataAlertDialog - onCreateAlert: { - panels.createNoDataAlert(alert_name, host_name, user_name, topic_name, threshold, t_between_triggers, contact_info) - } - } - - ScheduleClearDialog { id: scheduleClear } From 47826dc387a26173ac4151dd8eab9ce55ac5abe2 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 7 Oct 2025 11:43:40 +0200 Subject: [PATCH 19/42] WIP Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 4 +- include/fastdds_monitor/Engine.h | 6 +- .../fastdds_monitor/backend/AlertCallback.h | 18 ++ include/fastdds_monitor/backend/Listener.h | 4 + .../backend/SyncBackendConnection.h | 4 +- qml/AlertDialog.qml | 291 ++++++++++-------- qml/LeftPanel.qml | 4 +- qml/Panels.qml | 4 +- qml/main.qml | 2 +- src/Controller.cpp | 9 +- src/Engine.cpp | 27 +- src/backend/Listener.cpp | 7 + src/backend/SyncBackendConnection.cpp | 8 +- 13 files changed, 242 insertions(+), 146 deletions(-) diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 9cdf0d43..592e9532 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -235,13 +235,13 @@ public slots: //! 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, - QString contact_info); + int time_between_triggers); //! Give a string with the name of the unit magnitud in which each DataKind is measured QString get_data_kind_units( diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index b63dea3a..8e7afbad 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -500,13 +500,13 @@ class Engine : public QQmlApplicationEngine 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, - const std::string& contact_info); + const std::chrono::milliseconds& t_between_triggers); /** * This methods updates the info and summary if the entity clicked (the entity that is being shown) is the @@ -964,6 +964,8 @@ 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 diff --git a/include/fastdds_monitor/backend/AlertCallback.h b/include/fastdds_monitor/backend/AlertCallback.h index 62e1f1fd..efc98862 100644 --- a/include/fastdds_monitor/backend/AlertCallback.h +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -28,10 +28,25 @@ namespace backend { +enum AlertCallbackKind +{ + ALERT_TRIGGERED, + ALERT_UNMATCHED +}; + struct AlertCallback { AlertCallback() = default; + AlertCallback( + backend::EntityId domain_entity_id, + backend::AlertInfo alert_info) + : domain_id(domain_entity_id) + , alert_info(alert_info) + , kind(AlertCallbackKind::ALERT_UNMATCHED) + { + } + AlertCallback( backend::EntityId domain_entity_id, backend::EntityId entity_id, @@ -41,13 +56,16 @@ struct AlertCallback , entity_id(entity_id) , alert_info(alert_info) , trigger_data(trigger_data) + , kind(AlertCallbackKind::ALERT_TRIGGERED) { } + backend::EntityId domain_id; backend::EntityId entity_id; backend::AlertInfo alert_info; double trigger_data; + AlertCallbackKind kind; }; } // namespace backend diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index 91756cd9..a27723fc 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -104,6 +104,10 @@ class Listener : public PhysicalListener const AlertInfo& alert, const double& data) override; + void on_alert_unmatched( + EntityId domain_id, + const AlertInfo& alert) override; + protected: //! Engine reference diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 796f5e66..00bab64e 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -880,13 +880,13 @@ class SyncBackendConnection //! 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, - const std::string& contact_info); + const std::chrono::milliseconds& t_between_triggers); protected: diff --git a/qml/AlertDialog.qml b/qml/AlertDialog.qml index cbbfd55b..f5f37271 100644 --- a/qml/AlertDialog.qml +++ b/qml/AlertDialog.qml @@ -34,22 +34,22 @@ Dialog { property bool activeOk: true property string currentKind: "" property string currentAlertName: "" + property string currentDomain: "" property string currentHost: "" property string currentUser: "" property string currentTopic: "" - property int currentThreshold: 5 + property double currentThreshold: 0 property int currentTimeBetweenAlerts: 5000 - property string contactInfo: "sample@email.com" modal: false - title: "Create new alert" + title: "Add alert" standardButtons: Dialog.Ok | Dialog.Cancel x: (parent.width - width) / 2 y: (parent.height - height) / 2 signal createAlert(string alert_name, string host_name, string user_name, string topic_name, - string alert_type, int t_between_triggers, int threshold, string contact_info) + string alert_type, int t_between_triggers, int threshold) Component.onCompleted: { availableAlertKinds = controller.get_alert_kinds() @@ -63,20 +63,24 @@ Dialog { 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 = parseInt(alertThreshold.value) - createAlert(currentAlertName, currentHost, currentUser, currentTopic, currentKind, currentTimeBetweenAlerts, currentThreshold, contactInfo) + currentThreshold = parseFloat(alertThreshold.text) + + createAlert(currentAlertName, currentDomain, currentHost, currentUser, currentTopic, currentKind, currentTimeBetweenAlerts, currentThreshold) } onAboutToShow: { alertKindComboBox.currentIndex = -1 - alertNameTextField.text = "" + alertNameTextField.text = "" + domainComboBox.currentIndex = -1 hostComboBox.currentIndex = -1 topicComboBox.currentIndex = -1 userComboBox.currentIndex = -1 + updateDomains() updateTopics() updateUsers() updateHosts() @@ -128,18 +132,42 @@ Dialog { 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 name from which the data\n" + - "will be collected." + text: "Host watched by the alert." } } AdaptiveComboBox { id: hostComboBox - enabled: !manualHostCheckBox.checked + enabled: true textRole: "nameId" valueRole: "id" popup.y: height @@ -158,8 +186,7 @@ Dialog { id: userLabel text: "User: " InfoToolTip { - text: "User name from which the data\n" + - "will be collected." + text: "User watched by the alert." } } @@ -184,8 +211,7 @@ Dialog { id: topicLabel text: "Topic: " InfoToolTip { - text: "Topic name from which the data\n" + - "will be collected." + text: "Topic watched by the alert." } } @@ -304,138 +330,125 @@ Dialog { } } - Column { + 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_ - - Row { - id: manualHost - spacing: alertDialog.layout_vertical_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 - } + // 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 - height: 5/6*advancedOptionsSubmenu.item_height_ - anchors.verticalCenter: parent.verticalCenter + 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 - } + background: Rectangle { + color: !manualHostCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } - onTextChanged: { - } + onTextChanged: { } } - Row { - id: manualUser - spacing: alertDialog.layout_vertical_spacing_ - 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 - } + 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 - height: 5/6*advancedOptionsSubmenu.item_height_ - anchors.verticalCenter: parent.verticalCenter + 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 - } + background: Rectangle { + color: !manualUserCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } - onTextChanged: { - } + onTextChanged: { } } - Row { - id: manualTopic - spacing: alertDialog.layout_vertical_spacing_ - 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 - } + 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 - height: 5/6*advancedOptionsSubmenu.item_height_ - anchors.verticalCenter: parent.verticalCenter + 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 - } + background: Rectangle { + color: !manualTopicCheckBox.checked ? "#a0a0a0" : Theme.whiteSmoke + border.color: Theme.grey + } - onTextChanged: { - } + onTextChanged: { } } } @@ -443,6 +456,17 @@ Dialog { } } + 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" @@ -453,15 +477,38 @@ Dialog { 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() diff --git a/qml/LeftPanel.qml b/qml/LeftPanel.qml index 84c61a74..81a3c668 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -196,7 +196,7 @@ RowLayout { monitoringPanel.changeExplorerEntityInfo(status) } - function createAlert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info) { - controller.set_alert(name, hostId, userId, topicId, alert_type, threshold, t_between_triggers, contact_info); + 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); } } diff --git a/qml/Panels.qml b/qml/Panels.qml index 414b750a..a2fe187b 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -207,7 +207,7 @@ RowLayout { leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } - function createAlert(name, hostId, userId, topicId, alert_type, t_between_triggers, contact_info){ - leftPanel.createAlert(name, hostId, userId, topicId, alert_type, t_between_triggers, contact_info) + function createAlert(name, domainId, hostId, userId, topicId, alert_type, t_between_triggers){ + leftPanel.createAlert(name, domainId, hostId, userId, topicId, alert_type, t_between_triggers) } } diff --git a/qml/main.qml b/qml/main.qml index f8b76dda..c507e364 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -123,7 +123,7 @@ ApplicationWindow { AlertDialog { id: alertDialog onCreateAlert: { - panels.createAlert(alert_name, host_name, user_name, topic_name, alert_type, threshold, t_between_triggers, contact_info) + panels.createAlert(alert_name, domain_name, host_name, user_name, topic_name, alert_type, threshold, t_between_triggers) } } diff --git a/src/Controller.cpp b/src/Controller.cpp index ef65d8da..f3377018 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -314,27 +314,26 @@ std::string clean_entity_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, - QString contact_info) + 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), - utils::to_string(contact_info)); + std::chrono::milliseconds(time_between_triggers)); } QString Controller::get_data_kind_units( diff --git a/src/Engine.cpp b/src/Engine.cpp index fd978a1b..c080b689 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -123,6 +123,9 @@ QObject* Engine::enable() 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()); @@ -157,6 +160,7 @@ QObject* Engine::enable() 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_); @@ -286,6 +290,11 @@ 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_; @@ -895,6 +904,16 @@ 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(); @@ -1907,17 +1926,17 @@ 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, - const std::string& contact_info) + const std::chrono::milliseconds& t_between_triggers) { // Adding alert to backend structures - backend_connection_.set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, - t_between_triggers, contact_info); + backend_connection_.set_alert(alert_name, domain_id, host_name, user_name, topic_name, alert_kind, threshold, + t_between_triggers); } bool Engine::update_entity( diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index dad45dfa..4a430a04 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -186,4 +186,11 @@ void Listener::on_alert_triggered( engine_->add_callback(AlertCallback(domain_id, entity_id, alert, data)); } +void Listener::on_alert_unmatched( + EntityId domain_id, + const AlertInfo& alert) +{ + engine_->add_callback(AlertCallback(domain_id, alert)); +} + } //namespace backend diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 7830a513..ae88d724 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -1338,18 +1338,18 @@ 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, - const std::string& contact_info) + const std::chrono::milliseconds& t_between_triggers) { try { - StatisticsBackend::set_alert(alert_name, host_name, user_name, topic_name, alert_kind, threshold, - t_between_triggers, contact_info); + StatisticsBackend::set_alert(alert_name, domain_id, host_name, user_name, topic_name, alert_kind, threshold, + t_between_triggers); } catch (const Exception& e) { From 9387ec0771f5a0752dc05efba2c80c09ca54ecc4 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 7 Oct 2025 12:21:21 +0200 Subject: [PATCH 20/42] WIP, no warnings Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Engine.h | 6 ++-- .../backend/SyncBackendConnection.h | 10 ++---- src/Engine.cpp | 6 ++-- src/backend/SyncBackendConnection.cpp | 34 +++---------------- 4 files changed, 11 insertions(+), 45 deletions(-) diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 8e7afbad..7cfd2c57 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -769,10 +769,8 @@ public slots: std::string callback, std::string time); - //! Add a new alert object to the Alert model - bool add_alert_( - std::string alert, - 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_( diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 00bab64e..538cad0e 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -479,14 +479,10 @@ class SyncBackendConnection * @brief Update the alerts model with every alert in the backend * * @param alerts_model Alerts model to update - * @param inactive_visible Whether inactive alerts should be shown - * @param metatraffic_visible Whether metatraffic alerts should be shown * @return true if any change has been made, false otherwise */ bool update_alerts_model( - models::AlertListModel* alerts_model, - bool inactive_visible, - bool metatraffic_visible); + models::AlertListModel* alerts_model); ///// // Entity update functions @@ -670,9 +666,7 @@ class SyncBackendConnection bool proxy_visible); bool update_alert_item_( - AlertListItem* item, - bool inactive_visible, - bool metatraffic_visible); + AlertListItem* item); bool update_alert_item_info_( AlertListItem* item); diff --git a/src/Engine.cpp b/src/Engine.cpp index c080b689..e6697d77 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -482,7 +482,7 @@ bool Engine::fill_first_alert_summary_() bool Engine::fill_alert_list_() { alert_model_->clear(); - return backend_connection_.update_alerts_model(alert_model_, inactive_visible(), metatraffic_visible()); + return backend_connection_.update_alerts_model(alert_model_); } bool Engine::fill_alert_summary_( @@ -598,9 +598,7 @@ void Engine::clear_issue_info_() fill_issue_(); } -bool Engine::add_alert_( - std::string alert, - std::string time) +bool Engine::update_alerts_() { fill_alert_list_(); return true; diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index ae88d724..b27f7ec2 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -334,9 +334,7 @@ bool SyncBackendConnection::update_dds_model( } bool SyncBackendConnection::update_alert_item_( - AlertListItem* item, - bool inactive_visible, - bool metatraffic_visible) + AlertListItem* item) { bool res = update_alert_item_info_(item); return res; @@ -352,9 +350,7 @@ bool SyncBackendConnection::update_alert_item_info_( } bool SyncBackendConnection::update_alerts_model( - AlertListModel* alerts_model, - bool inactive_visible, - bool metatraffic_visible) + AlertListModel* alerts_model) { bool changed = false; @@ -370,38 +366,18 @@ bool SyncBackendConnection::update_alerts_model( // If it exists it updates its info if (index == -1) { - // Only create the new alert if is alive or inactive are visible - if ((inactive_visible || get_alive(alert_id))) - { // 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, inactive_visible, - metatraffic_visible) || changed; - } + changed = update_alert_item_(alert_item) || changed; } - - // In case this entity is inactive and inactive are not being displayed - else if ((!inactive_visible && !get_alive(alert_id))) - { - models::AlertListItem* alert_item = alerts_model->at(index); - - // Remove the row - alerts_model->removeRow(index); - - // Remove its subentities and the object ListItem - delete alert_item; - - changed = true; - } - - // Otherwise just update the entity else { + // Otherwise just update the entity models::AlertListItem* alert_item = alerts_model->at(index); - changed = update_alert_item_(alert_item, inactive_visible, metatraffic_visible) + changed = update_alert_item_(alert_item) || changed; } } From 9b120fcf32fab706bda05d3fbc8b6b945023121b Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 7 Oct 2025 12:51:53 +0200 Subject: [PATCH 21/42] WIP Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/backend/AlertCallback.h | 4 ++-- include/fastdds_monitor/backend/Listener.h | 4 ++-- qml/AlertDialog.qml | 4 ++-- qml/Panels.qml | 4 ++-- src/Engine.cpp | 4 ++-- src/backend/Listener.cpp | 4 ++-- src/backend/SyncBackendConnection.cpp | 2 -- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/include/fastdds_monitor/backend/AlertCallback.h b/include/fastdds_monitor/backend/AlertCallback.h index efc98862..345db6e5 100644 --- a/include/fastdds_monitor/backend/AlertCallback.h +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -40,7 +40,7 @@ struct AlertCallback AlertCallback( backend::EntityId domain_entity_id, - backend::AlertInfo alert_info) + backend::AlertInfo& alert_info) : domain_id(domain_entity_id) , alert_info(alert_info) , kind(AlertCallbackKind::ALERT_UNMATCHED) @@ -50,7 +50,7 @@ struct AlertCallback AlertCallback( backend::EntityId domain_entity_id, backend::EntityId entity_id, - backend::AlertInfo alert_info, + backend::AlertInfo& alert_info, double trigger_data) : domain_id(domain_entity_id) , entity_id(entity_id) diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index a27723fc..2129ce42 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -101,12 +101,12 @@ class Listener : public PhysicalListener void on_alert_triggered( EntityId domain_id, EntityId entity_id, - const AlertInfo& alert, + AlertInfo& alert, const double& data) override; void on_alert_unmatched( EntityId domain_id, - const AlertInfo& alert) override; + AlertInfo& alert) override; protected: diff --git a/qml/AlertDialog.qml b/qml/AlertDialog.qml index f5f37271..52bbca7a 100644 --- a/qml/AlertDialog.qml +++ b/qml/AlertDialog.qml @@ -48,8 +48,8 @@ Dialog { x: (parent.width - width) / 2 y: (parent.height - height) / 2 - signal createAlert(string alert_name, string host_name, string user_name, string topic_name, - string alert_type, int t_between_triggers, int threshold) + 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() diff --git a/qml/Panels.qml b/qml/Panels.qml index a2fe187b..43450a09 100644 --- a/qml/Panels.qml +++ b/qml/Panels.qml @@ -207,7 +207,7 @@ RowLayout { leftPanel.openTopicMenu(domainEntityId, domainId, entityId, currentAlias, entityKind, caller) } - function createAlert(name, domainId, hostId, userId, topicId, alert_type, t_between_triggers){ - leftPanel.createAlert(name, domainId, hostId, userId, topicId, alert_type, t_between_triggers) + 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) } } diff --git a/src/Engine.cpp b/src/Engine.cpp index e6697d77..2f13d6a6 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -1289,11 +1289,11 @@ bool Engine::read_callback_( { case backend::AlertKind::NEW_DATA: return add_alert_message_info_( - alert_callback.alert_info.get_alert_name(), "NEW_DATA alert triggered", utils::now()); + alert_callback.alert_info.get_alert_name(), "New data received, DATA_COUNT is " + std::to_string(alert_callback.trigger_data), utils::now()); break; case backend::AlertKind::NO_DATA: return add_alert_message_info_( - alert_callback.alert_info.get_alert_name(), "NO_DATA alert triggered", utils::now()); + alert_callback.alert_info.get_alert_name(), "SUBSCRIPTION_THROUGHPUT is " + std::to_string(alert_callback.trigger_data), utils::now()); break; case backend::AlertKind::INVALID: default: diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index 4a430a04..8ad8af69 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -180,7 +180,7 @@ void Listener::on_status_reported( void Listener::on_alert_triggered( EntityId domain_id, EntityId entity_id, - const AlertInfo& alert, + AlertInfo& alert, const double& data) { engine_->add_callback(AlertCallback(domain_id, entity_id, alert, data)); @@ -188,7 +188,7 @@ void Listener::on_alert_triggered( void Listener::on_alert_unmatched( EntityId domain_id, - const AlertInfo& alert) + AlertInfo& alert) { engine_->add_callback(AlertCallback(domain_id, alert)); } diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index b27f7ec2..6d410d38 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -357,8 +357,6 @@ bool SyncBackendConnection::update_alerts_model( // For each User get all processes for (auto& alert_id : get_alerts()) { - // AlertId alert_id = alert_tuple.first; - // AlertInfo alert_info = alert_tuple->second; // Check if it exists already int index = alerts_model->rowIndexFromId(alert_id); From 321c3add97265d85730d131d8181c1a48179e4d6 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 7 Oct 2025 15:04:25 +0200 Subject: [PATCH 22/42] Background check of alerts apparently working Signed-off-by: Emilio Cuesta --- .../fastdds_monitor/backend/AlertCallback.h | 24 ----------------- src/Engine.cpp | 5 ++++ src/backend/Listener.cpp | 14 ++++++++-- src/backend/backend_utils.cpp | 27 ++----------------- 4 files changed, 19 insertions(+), 51 deletions(-) diff --git a/include/fastdds_monitor/backend/AlertCallback.h b/include/fastdds_monitor/backend/AlertCallback.h index 345db6e5..bf56979c 100644 --- a/include/fastdds_monitor/backend/AlertCallback.h +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -37,30 +37,6 @@ enum AlertCallbackKind struct AlertCallback { AlertCallback() = default; - - AlertCallback( - backend::EntityId domain_entity_id, - backend::AlertInfo& alert_info) - : domain_id(domain_entity_id) - , alert_info(alert_info) - , kind(AlertCallbackKind::ALERT_UNMATCHED) - { - } - - AlertCallback( - backend::EntityId domain_entity_id, - backend::EntityId entity_id, - backend::AlertInfo& alert_info, - double trigger_data) - : domain_id(domain_entity_id) - , entity_id(entity_id) - , alert_info(alert_info) - , trigger_data(trigger_data) - , kind(AlertCallbackKind::ALERT_TRIGGERED) - { - } - - backend::EntityId domain_id; backend::EntityId entity_id; backend::AlertInfo alert_info; diff --git a/src/Engine.cpp b/src/Engine.cpp index 2f13d6a6..9dd78e26 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -1284,6 +1284,11 @@ bool Engine::read_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()) { diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index 8ad8af69..86d5cbb8 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -183,14 +183,24 @@ void Listener::on_alert_triggered( AlertInfo& alert, const double& data) { - engine_->add_callback(AlertCallback(domain_id, entity_id, alert, 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) { - engine_->add_callback(AlertCallback(domain_id, 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/backend_utils.cpp b/src/backend/backend_utils.cpp index 07ac925b..0394a324 100644 --- a/src/backend/backend_utils.cpp +++ b/src/backend/backend_utils.cpp @@ -105,37 +105,14 @@ models::AlertId alert_backend_id_to_models_id( const AlertId& id) { std::ostringstream stream; - // if (id == ID_ALL) - // { - // stream << models::ID_ALL; - // } - // else if (id == ID_NONE) - // { - // stream << models::ID_INVALID; - // } - // else - { - stream << id; - } + stream << id; return utils::to_QString(stream.str()); } AlertId alert_models_id_to_backend_id( const models::AlertId& id) { - std::ostringstream stream; - // if (id == models::ID_ALL) - // { - // return AlertId::all(); - // } - // else if (id == models::ID_INVALID || id == "") - // { - // return AlertId::invalid(); - // } - // else - { - return AlertId(id.toInt()); - } + return AlertId(id.toInt()); } QString alert_kind_to_QString( From 0810d44832a675f00490d2f5203825924690c5af Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Tue, 7 Oct 2025 16:03:39 +0200 Subject: [PATCH 23/42] WIP Signed-off-by: Emilio Cuesta --- qml/TopicMenu.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qml/TopicMenu.qml b/qml/TopicMenu.qml index f23460cb..38c012ea 100644 --- a/qml/TopicMenu.qml +++ b/qml/TopicMenu.qml @@ -48,10 +48,9 @@ Menu { onTriggered: openIDLView(menu.entityId) } MenuItem { - text: "Set New Data Alert" + text: "Set Alert" onTriggered: { - newDataAlertDialog.currentTopic = menu.entityId - newDataAlertDialog.open() + alertDialog.open() } } } From 99fea866aa2f34a45147afcc95f87c2c2e614f56 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 8 Oct 2025 07:38:26 +0200 Subject: [PATCH 24/42] Changing double to long double due to Windows complain Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/backend/Listener.h | 2 +- src/backend/Listener.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index 2129ce42..61ff1d65 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -102,7 +102,7 @@ class Listener : public PhysicalListener EntityId domain_id, EntityId entity_id, AlertInfo& alert, - const double& data) override; + const long double& data) override; void on_alert_unmatched( EntityId domain_id, diff --git a/src/backend/Listener.cpp b/src/backend/Listener.cpp index 86d5cbb8..81b031db 100644 --- a/src/backend/Listener.cpp +++ b/src/backend/Listener.cpp @@ -181,7 +181,7 @@ void Listener::on_alert_triggered( EntityId domain_id, EntityId entity_id, AlertInfo& alert, - const double& data) + const long double& data) { AlertCallback callback; callback.domain_id = domain_id; From 8d628b8def22ac2e06214d9bf00e48c7521f812c Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 8 Oct 2025 07:39:16 +0200 Subject: [PATCH 25/42] Uncrustify Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/backend/Listener.h | 4 ++-- src/Engine.cpp | 21 ++++++++++++++------- src/backend/SyncBackendConnection.cpp | 10 +++++----- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index 61ff1d65..1df0fa52 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -105,8 +105,8 @@ class Listener : public PhysicalListener const long double& data) override; void on_alert_unmatched( - EntityId domain_id, - AlertInfo& alert) override; + EntityId domain_id, + AlertInfo& alert) override; protected: diff --git a/src/Engine.cpp b/src/Engine.cpp index 9dd78e26..a3a981ff 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -963,7 +963,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, @@ -1286,7 +1287,8 @@ bool Engine::read_callback_( 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()); + 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 @@ -1294,11 +1296,13 @@ bool Engine::read_callback_( { case backend::AlertKind::NEW_DATA: return add_alert_message_info_( - alert_callback.alert_info.get_alert_name(), "New data received, DATA_COUNT is " + std::to_string(alert_callback.trigger_data), utils::now()); + alert_callback.alert_info.get_alert_name(), + "New data received, DATA_COUNT is " + std::to_string(alert_callback.trigger_data), utils::now()); break; case backend::AlertKind::NO_DATA: return add_alert_message_info_( - alert_callback.alert_info.get_alert_name(), "SUBSCRIPTION_THROUGHPUT is " + std::to_string(alert_callback.trigger_data), utils::now()); + alert_callback.alert_info.get_alert_name(), + "SUBSCRIPTION_THROUGHPUT is " + std::to_string(alert_callback.trigger_data), utils::now()); break; case backend::AlertKind::INVALID: default: @@ -1498,9 +1502,11 @@ bool Engine::update_entity_status( std::string(backend::policy_id_to_string(policy_id) + ":"), sample.status, "", std::string( - "Check for compatible rules ") + + "Check for compatible rules ") + + std::string( - "here"), @@ -1512,7 +1518,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(); diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 6d410d38..9a61da16 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -364,12 +364,12 @@ bool SyncBackendConnection::update_alerts_model( // 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); + // 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; + changed = update_alert_item_(alert_item) || changed; } else { From 13cdb94715763ef3fc16ec463a9d5ad7cd331d64 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 8 Oct 2025 14:30:23 +0200 Subject: [PATCH 26/42] Minor changes Signed-off-by: Emilio Cuesta --- qml/AlertDialog.qml | 2 +- qml/AlertList.qml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/qml/AlertDialog.qml b/qml/AlertDialog.qml index 52bbca7a..0d69e5f0 100644 --- a/qml/AlertDialog.qml +++ b/qml/AlertDialog.qml @@ -167,7 +167,7 @@ Dialog { AdaptiveComboBox { id: hostComboBox - enabled: true + enabled: !manualHostCheckBox.checked textRole: "nameId" valueRole: "id" popup.y: height diff --git a/qml/AlertList.qml b/qml/AlertList.qml index 10a45353..e848297c 100644 --- a/qml/AlertList.qml +++ b/qml/AlertList.qml @@ -65,12 +65,14 @@ Rectangle { 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 controller.alert_click(id) } } @@ -83,11 +85,11 @@ Rectangle { name: "alert" size: iconSize Layout.leftMargin: firstIndentation - color: entityLabelColor(clicked, alive) + color: entityLabelColor(alertHighlightRect.clicked, alive) } Label { text: name - color: entityLabelColor(clicked, alive) + color: entityLabelColor(alertHighlightRect.clicked, alive) } } } From 36f7ba3942bb80375eb05b5ec7c9fe06628f6187 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 8 Oct 2025 18:24:51 +0200 Subject: [PATCH 27/42] Adding remove alert functionality Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Controller.h | 4 ++ include/fastdds_monitor/Engine.h | 5 +++ .../backend/SyncBackendConnection.h | 4 ++ qml.qrc | 1 + qml/AlertList.qml | 6 ++- qml/AlertsMenu.qml | 40 +++++++++++++++++++ qml/EntitiesMenu.qml | 10 ++--- qml/LeftPanel.qml | 17 +++++++- qml/Panels.qml | 12 ++++++ qml/TabLayout.qml | 1 + src/Controller.cpp | 5 +++ src/Engine.cpp | 6 +++ src/backend/SyncBackendConnection.cpp | 14 +++++++ 13 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 qml/AlertsMenu.qml diff --git a/include/fastdds_monitor/Controller.h b/include/fastdds_monitor/Controller.h index 592e9532..de68b792 100644 --- a/include/fastdds_monitor/Controller.h +++ b/include/fastdds_monitor/Controller.h @@ -243,6 +243,10 @@ public slots: 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); diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index 7cfd2c57..d036e956 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -498,6 +498,7 @@ 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, @@ -508,6 +509,10 @@ class Engine : public QQmlApplicationEngine 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. diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index 538cad0e..e897757d 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -882,6 +882,10 @@ class SyncBackendConnection 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/qml.qrc b/qml.qrc index c4c83aa0..8a533803 100644 --- a/qml.qrc +++ b/qml.qrc @@ -18,6 +18,7 @@ qml/AdaptiveMenu.qml qml/AlertDialog.qml qml/AlertList.qml + qml/AlertsMenu.qml qml/AlertsPanel.qml qml/AlertSummary.qml qml/ChangeAliasDialog.qml diff --git a/qml/AlertList.qml b/qml/AlertList.qml index e848297c..db9caee2 100644 --- a/qml/AlertList.qml +++ b/qml/AlertList.qml @@ -73,7 +73,11 @@ Rectangle { onClicked: { alertHighlightRect.clicked = !alertHighlightRect.clicked - controller.alert_click(id) + if(mouse.button & Qt.RightButton) { + openAlertsMenu(id) + } else { + controller.alert_click(id) + } } } 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/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/LeftPanel.qml b/qml/LeftPanel.qml index 81a3c668..71c38236 100644 --- a/qml/LeftPanel.qml +++ b/qml/LeftPanel.qml @@ -51,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 @@ -83,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 @@ -119,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 @@ -199,4 +210,8 @@ RowLayout { 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/Panels.qml b/qml/Panels.qml index 43450a09..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 @@ -207,7 +211,15 @@ RowLayout { 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/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/src/Controller.cpp b/src/Controller.cpp index f3377018..aee5bb31 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -336,6 +336,11 @@ void Controller::set_alert( 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) { diff --git a/src/Engine.cpp b/src/Engine.cpp index a3a981ff..c19faad3 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -1949,6 +1949,12 @@ void Engine::set_alert( t_between_triggers); } +void Engine::remove_alert( + const backend::AlertId& id) +{ + backend_connection_.remove_alert(id); +} + bool Engine::update_entity( const backend::EntityId& entity_updated, bool (Engine::* update_function)(const backend::EntityId&, bool, bool), diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index 9a61da16..ea7c1601 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -1332,6 +1332,20 @@ void SyncBackendConnection::set_alert( } } +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, From 9299c1c809ecb856e955977743259702f1c98292 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 8 Oct 2025 18:26:52 +0200 Subject: [PATCH 28/42] Uncrustify and removing copyright Signed-off-by: Emilio Cuesta --- src/Controller.cpp | 3 ++- src/backend/SyncBackendConnection.cpp | 2 +- src/model/alerts/AlertListItem.cpp | 27 --------------------------- 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/src/Controller.cpp b/src/Controller.cpp index aee5bb31..5d0c1c04 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -336,7 +336,8 @@ void Controller::set_alert( std::chrono::milliseconds(time_between_triggers)); } -void Controller::remove_alert(QString id) +void Controller::remove_alert( + QString id) { engine_->remove_alert(backend::alert_models_id_to_backend_id(id)); } diff --git a/src/backend/SyncBackendConnection.cpp b/src/backend/SyncBackendConnection.cpp index ea7c1601..60a6a9fa 100644 --- a/src/backend/SyncBackendConnection.cpp +++ b/src/backend/SyncBackendConnection.cpp @@ -1333,7 +1333,7 @@ void SyncBackendConnection::set_alert( } void SyncBackendConnection::remove_alert( - const backend::AlertId& id) + const backend::AlertId& id) { try { diff --git a/src/model/alerts/AlertListItem.cpp b/src/model/alerts/AlertListItem.cpp index 15b357c8..9c56ebcc 100644 --- a/src/model/alerts/AlertListItem.cpp +++ b/src/model/alerts/AlertListItem.cpp @@ -1,30 +1,3 @@ -/**************************************************************************** -** -** Copyright (C) Paul Lemire, Tepee3DTeam and/or its subsidiary(-ies). -** Contact: paul.lemire@epitech.eu -** Contact: tepee3d_2014@labeip.epitech.eu -** -** This file is part of the Tepee3D project -** -** GNU Lesser General Public License Usage -** This file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** -****************************************************************************/ - // Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). // // This file is part of eProsima Fast DDS Monitor. From 8e519faa21ef4f3fec9b894ab6d3ea0211c24d1b Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 8 Oct 2025 18:52:47 +0200 Subject: [PATCH 29/42] Applying review Signed-off-by: Emilio Cuesta --- include/fastdds_monitor/Engine.h | 5 +--- .../fastdds_monitor/backend/AlertCallback.h | 2 +- include/fastdds_monitor/backend/Listener.h | 1 + .../backend/SyncBackendConnection.h | 1 - .../fastdds_monitor/backend/backend_types.h | 1 - .../model/alerts/AlertListModel.h | 2 +- include/fastdds_monitor/model/tree/TreeItem.h | 1 - .../fastdds_monitor/model/tree/TreeModel.h | 2 +- src/Engine.cpp | 6 +---- src/backend/SyncBackendConnection.cpp | 3 +-- src/model/alerts/AlertListModel.cpp | 27 ------------------- 11 files changed, 7 insertions(+), 44 deletions(-) diff --git a/include/fastdds_monitor/Engine.h b/include/fastdds_monitor/Engine.h index d036e956..75dfc5b4 100644 --- a/include/fastdds_monitor/Engine.h +++ b/include/fastdds_monitor/Engine.h @@ -283,7 +283,6 @@ class Engine : public QQmlApplicationEngine const backend::EntityId& id, backend::StatusKind kind); - /** * @brief Update the entity status counters and populate the model with empty message if empty * @@ -339,7 +338,6 @@ class Engine : public QQmlApplicationEngine bool update_dds = true, bool reset_dds = true); - /** * @brief Call the event chain when an alert is clicked * @@ -400,7 +398,7 @@ class Engine : public QQmlApplicationEngine backend::StatusCallback callback); /** - * @brief add an alert callback arrived from the backend to the alert callback queue + * @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. @@ -823,7 +821,6 @@ public slots: */ void generate_new_status_info_(); - //! Update the issue model "Entities" count adding \c n void sum_entity_number_issue( int n); diff --git a/include/fastdds_monitor/backend/AlertCallback.h b/include/fastdds_monitor/backend/AlertCallback.h index bf56979c..e8dcbd2a 100644 --- a/include/fastdds_monitor/backend/AlertCallback.h +++ b/include/fastdds_monitor/backend/AlertCallback.h @@ -1,4 +1,4 @@ -// Copyright 2023 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). // // This file is part of eProsima Fast DDS Monitor. // diff --git a/include/fastdds_monitor/backend/Listener.h b/include/fastdds_monitor/backend/Listener.h index 1df0fa52..1001d0ee 100644 --- a/include/fastdds_monitor/backend/Listener.h +++ b/include/fastdds_monitor/backend/Listener.h @@ -104,6 +104,7 @@ class Listener : public PhysicalListener AlertInfo& alert, const long double& data) override; + //! Callback when an alert is unmatched void on_alert_unmatched( EntityId domain_id, AlertInfo& alert) override; diff --git a/include/fastdds_monitor/backend/SyncBackendConnection.h b/include/fastdds_monitor/backend/SyncBackendConnection.h index e897757d..2072a9d0 100644 --- a/include/fastdds_monitor/backend/SyncBackendConnection.h +++ b/include/fastdds_monitor/backend/SyncBackendConnection.h @@ -474,7 +474,6 @@ class SyncBackendConnection bool metatraffic_visible, bool proxy_visible); - /** * @brief Update the alerts model with every alert in the backend * diff --git a/include/fastdds_monitor/backend/backend_types.h b/include/fastdds_monitor/backend/backend_types.h index 957a0a91..1df185bc 100644 --- a/include/fastdds_monitor/backend/backend_types.h +++ b/include/fastdds_monitor/backend/backend_types.h @@ -31,7 +31,6 @@ #include #include - namespace backend { //! Add a type of each kind with same name under \c backend namespace diff --git a/include/fastdds_monitor/model/alerts/AlertListModel.h b/include/fastdds_monitor/model/alerts/AlertListModel.h index a5a6d43d..917dde9e 100644 --- a/include/fastdds_monitor/model/alerts/AlertListModel.h +++ b/include/fastdds_monitor/model/alerts/AlertListModel.h @@ -1,4 +1,4 @@ -// Copyright 2021 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). // // This file is part of eProsima Fast DDS Monitor. // diff --git a/include/fastdds_monitor/model/tree/TreeItem.h b/include/fastdds_monitor/model/tree/TreeItem.h index e58c60d7..3ca9562e 100644 --- a/include/fastdds_monitor/model/tree/TreeItem.h +++ b/include/fastdds_monitor/model/tree/TreeItem.h @@ -63,7 +63,6 @@ class TreeItem TreeItem* child_item( int row); - void remove_child_item( int row); diff --git a/include/fastdds_monitor/model/tree/TreeModel.h b/include/fastdds_monitor/model/tree/TreeModel.h index 7f5e48d8..d529a0fa 100644 --- a/include/fastdds_monitor/model/tree/TreeModel.h +++ b/include/fastdds_monitor/model/tree/TreeModel.h @@ -164,7 +164,7 @@ class TreeModel : public QAbstractItemModel const json& json_data); /** - * @brief Iterates over the children of a node to find one with a specific name + * @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 diff --git a/src/Engine.cpp b/src/Engine.cpp index c19faad3..3c719420 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -123,7 +123,6 @@ QObject* Engine::enable() 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()); @@ -290,7 +289,6 @@ Engine::~Engine() delete destination_entity_id_model_; } - if (alert_domain_id_model_) { delete alert_domain_id_model_; @@ -308,7 +306,6 @@ Engine::~Engine() delete alert_topic_id_model_; } - // Interactive models if (historic_statistics_data_) { @@ -1502,8 +1499,7 @@ bool Engine::update_entity_status( std::string(backend::policy_id_to_string(policy_id) + ":"), sample.status, "", std::string( - "Check for compatible rules ") - + + "Check for compatible rules ") + std::string( "at(index); - changed = update_alert_item_(alert_item) - || changed; + changed = update_alert_item_(alert_item) || changed; } } diff --git a/src/model/alerts/AlertListModel.cpp b/src/model/alerts/AlertListModel.cpp index 2745b12f..2c78e068 100644 --- a/src/model/alerts/AlertListModel.cpp +++ b/src/model/alerts/AlertListModel.cpp @@ -1,30 +1,3 @@ -/**************************************************************************** -** -** Copyright (C) Paul Lemire, Tepee3DTeam and/or its subsidiary(-ies). -** Contact: paul.lemire@epitech.eu -** Contact: tepee3d_2014@labeip.epitech.eu -** -** This file is part of the Tepee3D project -** -** GNU Lesser General Public License Usage -** This file may be used under the terms of the GNU Lesser -** General Public License version 2.1 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 2.1 requirements -** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3.0 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. Please review the following information to -** ensure the GNU General Public License version 3.0 requirements will be -** met: http://www.gnu.org/copyleft/gpl.html. -** -** -****************************************************************************/ - // Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). // // This file is part of eProsima Fast DDS Monitor. From 3ee4328dcfdbc483a9db31d8c5dbc389ca449de1 Mon Sep 17 00:00:00 2001 From: Emilio Cuesta Date: Wed, 8 Oct 2025 20:57:26 +0200 Subject: [PATCH 30/42] Add documentation Signed-off-by: Emilio Cuesta --- docs/rst/exports/alias.include | 4 ++ .../screenshots/alert_messages_panel.png | Bin 0 -> 91416 bytes docs/rst/figures/screenshots/alert_panel.png | Bin 0 -> 33939 bytes .../usage_example/alert_dialog.png | Bin 0 -> 62520 bytes .../usage_example/alert_panel_post.png | Bin 0 -> 33939 bytes .../usage_example/alert_panel_pre.png | Bin 0 -> 8780 bytes docs/rst/getting_started/tutorial.rst | 30 ++++++++++++ docs/rst/user_manual/alert_messages_panel.rst | 12 +++++ docs/rst/user_manual/alerts_panel.rst | 26 +++++++++++ docs/rst/user_manual/layout.rst | 43 ++++++++++++++++++ docs/rst/user_manual/shortcuts_bar.rst | 1 + 11 files changed, 116 insertions(+) create mode 100644 docs/rst/figures/screenshots/alert_messages_panel.png create mode 100644 docs/rst/figures/screenshots/alert_panel.png create mode 100644 docs/rst/figures/screenshots/usage_example/alert_dialog.png create mode 100644 docs/rst/figures/screenshots/usage_example/alert_panel_post.png create mode 100644 docs/rst/figures/screenshots/usage_example/alert_panel_pre.png create mode 100644 docs/rst/user_manual/alert_messages_panel.rst create mode 100644 docs/rst/user_manual/alerts_panel.rst 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 0000000000000000000000000000000000000000..2eb41a2969f0257cb2b6cccee4c0965aee3759ed GIT binary patch literal 91416 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 HZQlJ4uPtpX literal 0 HcmV?d00001 diff --git a/docs/rst/figures/screenshots/alert_panel.png b/docs/rst/figures/screenshots/alert_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..afc4826aae0d17684ff050fbf940b0b9b78cdbc6 GIT binary patch literal 33939 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..afc4826aae0d17684ff050fbf940b0b9b78cdbc6 GIT binary patch literal 33939 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 z