From 32f08120c2b84fb8ae98c413fbb5ea35f098b93f Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 24 Apr 2023 23:59:49 +0200 Subject: [PATCH] Improve new logging stuff further --- libnymea-app/models/newlogsmodel.cpp | 87 ++++++++-- libnymea-app/models/newlogsmodel.h | 18 +- libnymea-app/thingmanager.cpp | 8 +- libnymea-app/types/thing.cpp | 13 ++ libnymea-app/types/thing.h | 6 + nymea-app/resources.qrc | 2 + nymea-app/ui/devicepages/ActionLogPage.qml | 163 ++++++++++++++++++ nymea-app/ui/devicepages/ButtonThingPage.qml | 5 +- nymea-app/ui/devicepages/EventLogPage.qml | 149 ++++++++++++++++ nymea-app/ui/devicepages/GenericThingPage.qml | 15 +- .../ui/devicepages/NotificationsThingPage.qml | 1 + nymea-app/ui/devicepages/StateLogPage.qml | 1 + 12 files changed, 443 insertions(+), 25 deletions(-) create mode 100644 nymea-app/ui/devicepages/ActionLogPage.qml create mode 100644 nymea-app/ui/devicepages/EventLogPage.qml diff --git a/libnymea-app/models/newlogsmodel.cpp b/libnymea-app/models/newlogsmodel.cpp index 6e031999..d04398ff 100644 --- a/libnymea-app/models/newlogsmodel.cpp +++ b/libnymea-app/models/newlogsmodel.cpp @@ -1,6 +1,7 @@ #include "newlogsmodel.h" #include "engine.h" +#include "logmanager.h" #include "logging.h" //NYMEA_LOGGING_CATEGORY(dcLogEngine, "LogEngine") @@ -58,7 +59,8 @@ void NewLogsModel::componentComplete() bool NewLogsModel::canFetchMore(const QModelIndex &parent) const { Q_UNUSED(parent) - return m_canFetchMore && m_sources.count() == 1; + // Cannot fetchMore when there are multiple sources as paging doesn't really work in that case + return m_canFetchMore && (m_sources.count() == 1 || m_list.isEmpty()); } void NewLogsModel::fetchMore(const QModelIndex &parent) @@ -83,13 +85,19 @@ Engine *NewLogsModel::engine() const void NewLogsModel::setEngine(Engine *engine) { - if (m_engine != engine) { - m_engine = engine; - emit engineChanged(); + if (m_engine == engine) { + return; + } -// if (m_completed && m_canFetchMore) { -// fetchMore(); -// } + if (m_engine) { + disconnect(m_engine->logManager(), &LogManager::logEntryReceived, this, &NewLogsModel::newLogEntryReceived); + } + + m_engine = engine; + emit engineChanged(); + + if (m_engine) { + connect(m_engine->logManager(), &LogManager::logEntryReceived, this, &NewLogsModel::newLogEntryReceived); } } @@ -203,6 +211,32 @@ bool NewLogsModel::busy() const return m_busy; } +bool NewLogsModel::live() const +{ + return m_live; +} + +void NewLogsModel::setLive(bool live) +{ + if (m_live != live) { + m_live = live; + emit liveChanged(); + } +} + +int NewLogsModel::fetchBlockSize() const +{ + return m_blockSize; +} + +void NewLogsModel::setFetchBlockSize(int fetchBlockSize) +{ + if (m_blockSize != fetchBlockSize) { + m_blockSize = fetchBlockSize; + emit fetchBlockSizeChanged(); + } +} + NewLogEntry *NewLogsModel::get(int index) const { if (index < 0 || index >= m_list.count()) { @@ -266,6 +300,8 @@ void NewLogsModel::clear() beginResetModel(); qDeleteAll(m_list); m_list.clear(); + m_currentNewest = QDateTime(); + m_lastOffset = 0; endResetModel(); emit countChanged(); emit entriesRemoved(0, count); @@ -292,17 +328,13 @@ void NewLogsModel::fetchLogs() } else { params.insert("limit", m_blockSize); if (m_list.count() > 0) { - params.insert("offset", m_lastOffset); - if (m_lastOffset == 0) { - if (m_endTime.isNull()) { - m_currentNewest = QDateTime::currentDateTime(); - } else { - m_currentNewest = m_endTime; - } + if (m_currentNewest.isNull()) { + m_currentNewest = QDateTime::currentDateTime(); } + params.insert("offset", m_lastOffset); params.insert("endTime", m_currentNewest.toMSecsSinceEpoch()); - m_lastOffset += m_blockSize; } + m_lastOffset += m_blockSize; } } else { @@ -367,8 +399,31 @@ void NewLogsModel::logsReply(int commandId, const QVariantMap &data) }); m_list.append(entries); endInsertRows(); - emit entriesAdded(m_list.count(), entries); + emit entriesAdded(m_list.count() - entries.count(), entries); } emit countChanged(); } + +void NewLogsModel::newLogEntryReceived(const QVariantMap &map) +{ + QString source = map.value("source").toString(); + QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(map.value("timestamp").toULongLong()); + QVariantMap values = map.value("values").toMap(); + + if (m_sources.contains(source) && m_sampleRate == SampleRateAny) { + NewLogEntry *entry = new NewLogEntry(source, timestamp, values, this); + if (m_sortOrder == Qt::AscendingOrder) { + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(entry); + endInsertRows(); + emit entriesAdded(m_list.count() - 1, {entry}); + } else { + beginInsertRows(QModelIndex(), 0, 0); + m_list.prepend(entry); + endInsertRows(); + emit entriesAdded(0, {entry}); + } + emit countChanged(); + } +} diff --git a/libnymea-app/models/newlogsmodel.h b/libnymea-app/models/newlogsmodel.h index 183757bc..aad235f2 100644 --- a/libnymea-app/models/newlogsmodel.h +++ b/libnymea-app/models/newlogsmodel.h @@ -21,9 +21,11 @@ class NewLogsModel : public QAbstractListModel, public QQmlParserStatus Q_PROPERTY(SampleRate sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged) Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) + Q_PROPERTY(int fetchBlockSize READ fetchBlockSize WRITE setFetchBlockSize NOTIFY fetchBlockSizeChanged) + Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) -// Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged) public: enum Role { @@ -85,6 +87,12 @@ public: bool busy() const; + bool live() const; + void setLive(bool live); + + int fetchBlockSize() const; + void setFetchBlockSize(int fetchBlockSize); + Q_INVOKABLE NewLogEntry *get(int index) const; Q_INVOKABLE NewLogEntry *find(const QDateTime ×tamp) const; @@ -107,11 +115,15 @@ signals: void sampleRateChanged(); void sortOrderChanged(); + void liveChanged(); + void fetchBlockSizeChanged(); + void entriesAdded(int index, const QList &entries); void entriesRemoved(int index, int count); private slots: void logsReply(int commandId, const QVariantMap &data); + void newLogEntryReceived(const QVariantMap &map); private: Engine *m_engine = nullptr; @@ -122,6 +134,8 @@ private: bool m_busy = false; + bool m_live = true; + // For time based sampling QDateTime m_startTime; QDateTime m_endTime; @@ -130,7 +144,7 @@ private: // For continuous scrolling lists bool m_completed = false; bool m_canFetchMore = true; - int m_blockSize = 5; + int m_blockSize = 50; int m_lastOffset = 0; QDateTime m_currentNewest; diff --git a/libnymea-app/thingmanager.cpp b/libnymea-app/thingmanager.cpp index ee0914b7..511b60cb 100644 --- a/libnymea-app/thingmanager.cpp +++ b/libnymea-app/thingmanager.cpp @@ -973,12 +973,14 @@ Thing* ThingManager::unpackThing(ThingManager *thingManager, const QVariantMap & loggedEventTypeIds.append(uuid.toUuid()); } thing->setLoggedEventTypeIds(loggedEventTypeIds); + QList loggedActionTypeIds; + foreach (const QVariant &uuid, thingMap.value("loggedActionTypeIds").toList()) { + loggedActionTypeIds.append(uuid.toUuid()); + } + thing->setLoggedActionTypeIds(loggedActionTypeIds); return thing; } - - - QVariantMap ThingManager::packParam(Param *param) { QVariantMap ret; diff --git a/libnymea-app/types/thing.cpp b/libnymea-app/types/thing.cpp index f99b22fd..5bda06e4 100644 --- a/libnymea-app/types/thing.cpp +++ b/libnymea-app/types/thing.cpp @@ -244,6 +244,19 @@ void Thing::setLoggedEventTypeIds(const QList &loggedEventTypeIds) } } +QList Thing::loggedActionTypeIds() const +{ + return m_loggedActionTypeIds; +} + +void Thing::setLoggedActionTypeIds(const QList &loggedActionTypeIds) +{ + if (m_loggedActionTypeIds != loggedActionTypeIds) { + m_loggedActionTypeIds = loggedActionTypeIds; + emit loggedActionTypeIdsChanged(); + } +} + int Thing::executeAction(const QString &actionName, const QVariantList ¶ms) { ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName); diff --git a/libnymea-app/types/thing.h b/libnymea-app/types/thing.h index be63eb35..aa347c7f 100644 --- a/libnymea-app/types/thing.h +++ b/libnymea-app/types/thing.h @@ -57,6 +57,7 @@ class Thing : public QObject Q_PROPERTY(ThingClass *thingClass READ thingClass CONSTANT) Q_PROPERTY(QList loggedStateTypeIds READ loggedStateTypeIds NOTIFY loggedStateTypeIdsChanged) Q_PROPERTY(QList loggedEventTypeIds READ loggedEventTypeIds NOTIFY loggedEventTypeIdsChanged) + Q_PROPERTY(QList loggedActionTypeIds READ loggedActionTypeIds NOTIFY loggedActionTypeIdsChanged) public: enum ThingSetupStatus { @@ -130,6 +131,9 @@ public: QList loggedEventTypeIds() const; void setLoggedEventTypeIds(const QList &loggedEventTypeIds); + QList loggedActionTypeIds() const; + void setLoggedActionTypeIds(const QList &loggedActionTypeIds); + ThingClass *thingClass() const; Q_INVOKABLE bool hasState(const QUuid &stateTypeId) const; @@ -150,6 +154,7 @@ signals: void statesChanged(); void loggedStateTypeIdsChanged(); void loggedEventTypeIdsChanged(); + void loggedActionTypeIdsChanged(); void eventTriggered(const QUuid &eventTypeId, const QVariantList ¶ms); signals: @@ -168,6 +173,7 @@ protected: ThingClass *m_thingClass = nullptr; QList m_loggedStateTypeIds; QList m_loggedEventTypeIds; + QList m_loggedActionTypeIds; QList m_pendingActions; }; diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 0a9fa341..788f6237 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -312,5 +312,7 @@ ui/customviews/StateChart.qml ui/devicepages/ThingLogPage.qml ui/devicepages/NotificationsThingPage.qml + ui/devicepages/ActionLogPage.qml + ui/devicepages/EventLogPage.qml diff --git a/nymea-app/ui/devicepages/ActionLogPage.qml b/nymea-app/ui/devicepages/ActionLogPage.qml new file mode 100644 index 00000000..6923198b --- /dev/null +++ b/nymea-app/ui/devicepages/ActionLogPage.qml @@ -0,0 +1,163 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../customviews" + +Page { + id: root + + property Thing thing: null + property ActionType actionType: null + + readonly property bool isLogged: thing.loggedActionTypeIds.indexOf(actionType.id) >= 0 + + header: NymeaHeader { + text: qsTr("History for %1").arg(root.actionType.displayName) + onBackPressed: pageStack.pop() + + HeaderButton { + imageSource: "delete" + visible: root.isLogged + onClicked: { + var popup = deleteLogsComponent.createObject(root) + popup.open() + } + + Component { + id: deleteLogsComponent + NymeaDialog { + title: qsTr("Remove logs?") + text: qsTr("Do you want to remove the log for this action and disable logging?") + standardButtons: Dialog.No | Dialog.Yes + onAccepted: engine.thingManager.setActionLogging(root.thing.id, root.actionType.id, false) + } + } + } + } + + NewLogsModel { + id: logsModel + engine: _engine + source: "action-" + root.thing.id + "-" + root.actionType.name + sortOrder: Qt.DescendingOrder + } + + + ListView { + id: listView + visible: root.isLogged + model: logsModel + clip: true + anchors.fill: parent + ScrollBar.vertical: ScrollBar {} + + delegate: NymeaItemDelegate { + id: delegate + width: listView.width + height: contentColumn.implicitHeight + Style.margins + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + property NewLogEntry entry: logsModel.get(index) + + contentItem: RowLayout { + id: contentColumn + anchors { left: parent.left; right: parent.right; margins: app.margins / 2 } + ColorIcon { + Layout.preferredWidth: Style.iconSize + Layout.preferredHeight: width + Layout.alignment: Qt.AlignVCenter + color: delegate.entry.values.status === "ThingErrorNoError" + ? Style.iconColor + : Style.red + name: delegate.entry.values.triggeredBy === "TriggeredByUser" + ? "account" + : "magic" + } + ColumnLayout { + RowLayout { + Label { + Layout.fillWidth: true + text: (delegate.entry.values.triggeredBy === "TriggeredByUser" ? qsTr("User action") : qsTr("Automation")) + + " - " + + (delegate.entry.values.status === "ThingErrorNoError" ? qsTr("success") : qsTr("Failure: %1").arg(delegate.entry.values.status)) + elide: Text.ElideRight + } + Label { + text: Qt.formatDateTime(model.timestamp,"dd.MM.yy - hh:mm:ss") + elide: Text.ElideRight + font: Style.smallFont + } + } + + Label { + Layout.fillWidth: true + text: { + var ret = [] + var values = JSON.parse(entry.values.params) + for (var i = 0; i < root.actionType.paramTypes.count; i++) { + var paramType = root.actionType.paramTypes.get(i) + ret.push(paramType.displayName + ": " + Types.toUiValue(values[paramType.name], paramType.unit) + " " + Types.toUiUnit(paramType.unit)) + } + return ret.join("
") + } + textFormat: Text.RichText + elide: Text.ElideRight + font: Style.smallFont + visible: text.length > 0 + } + } + } + + } + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + title: qsTr("Logging not enabled") + text: qsTr("This state is not being logged.") + imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) + buttonText: qsTr("Enable logging") + visible: !root.isLogged + onButtonClicked: { + print("enabming logging") + engine.thingManager.setStateLogging(root.thing.id, root.stateType.id, true) + } + } +} + diff --git a/nymea-app/ui/devicepages/ButtonThingPage.qml b/nymea-app/ui/devicepages/ButtonThingPage.qml index a24db2c9..97989200 100644 --- a/nymea-app/ui/devicepages/ButtonThingPage.qml +++ b/nymea-app/ui/devicepages/ButtonThingPage.qml @@ -68,9 +68,12 @@ ThingPageBase { id: logsModel engine: _engine sources: ["event-" + root.thing.id + "-pressed", "event-" + root.thing.id + "-longPressed"] -// live: true + live: true + fetchBlockSize: 200 // As paging doesn't work with multiple sources } + Component.onCompleted: print("**************** created", logsModel.sources) + delegate: NymeaItemDelegate { id: entryDelegate width: logView.width diff --git a/nymea-app/ui/devicepages/EventLogPage.qml b/nymea-app/ui/devicepages/EventLogPage.qml new file mode 100644 index 00000000..f08d3067 --- /dev/null +++ b/nymea-app/ui/devicepages/EventLogPage.qml @@ -0,0 +1,149 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2023, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project 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 +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../customviews" + +Page { + id: root + + property Thing thing: null + property EventType eventType: null + + readonly property bool isLogged: thing.loggedEventTypeIds.indexOf(eventType.id) >= 0 + + header: NymeaHeader { + text: qsTr("History for %1").arg(root.eventType.displayName) + onBackPressed: pageStack.pop() + + HeaderButton { + imageSource: "delete" + visible: root.isLogged + onClicked: { + var popup = deleteLogsComponent.createObject(root) + popup.open() + } + + Component { + id: deleteLogsComponent + NymeaDialog { + title: qsTr("Remove logs?") + text: qsTr("Do you want to remove the log for this event and disable logging?") + standardButtons: Dialog.No | Dialog.Yes + onAccepted: engine.thingManager.setEventLogging(root.thing.id, root.eventType.id, false) + } + } + } + } + + NewLogsModel { + id: logsModel + engine: _engine + source: "event-" + root.thing.id + "-" + root.eventType.name + sortOrder: Qt.DescendingOrder + } + + + ListView { + id: listView + visible: root.isLogged + model: logsModel + clip: true + anchors.fill: parent + ScrollBar.vertical: ScrollBar {} + + delegate: NymeaItemDelegate { + id: delegate + width: listView.width + height: contentColumn.implicitHeight + Style.margins + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + property NewLogEntry entry: logsModel.get(index) + + contentItem: RowLayout { + id: contentColumn + anchors { left: parent.left; right: parent.right; margins: app.margins / 2 } + ColorIcon { + Layout.preferredWidth: Style.iconSize + Layout.preferredHeight: width + Layout.alignment: Qt.AlignVCenter + name: "event" + } + ColumnLayout { + Label { + Layout.fillWidth: true + text: Qt.formatDateTime(model.timestamp,"dd.MM.yy - hh:mm:ss") + elide: Text.ElideRight + } + + Label { + Layout.fillWidth: true + text: { + var ret = [] + var values = JSON.parse(entry.values.params) + for (var i = 0; i < root.eventType.paramTypes.count; i++) { + var paramType = root.eventType.paramTypes.get(i) + ret.push(paramType.displayName + ": " + Types.toUiValue(values[paramType.name], paramType.unit) + " " + Types.toUiUnit(paramType.unit)) + } + return ret.join("
") + } + textFormat: Text.RichText + elide: Text.ElideRight + font: Style.smallFont + visible: text.length > 0 + } + } + } + + } + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + title: qsTr("Logging not enabled") + text: qsTr("This state is not being logged.") + imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) + buttonText: qsTr("Enable logging") + visible: !root.isLogged + onButtonClicked: { + print("enabming logging") + engine.thingManager.setStateLogging(root.thing.id, root.stateType.id, true) + } + } +} + diff --git a/nymea-app/ui/devicepages/GenericThingPage.qml b/nymea-app/ui/devicepages/GenericThingPage.qml index bd763ef6..3962fefa 100644 --- a/nymea-app/ui/devicepages/GenericThingPage.qml +++ b/nymea-app/ui/devicepages/GenericThingPage.qml @@ -145,7 +145,13 @@ ThingPageBase { swipe.close(); print("opening logs for", delegate.stateType) if (engine.jsonRpcClient.ensureServerVersion("8.0")) { - pageStack.push(Qt.resolvedUrl("StateLogPage.qml"), {thing: root.thing, stateType: delegate.stateType}) + if (delegate.stateType) { + pageStack.push(Qt.resolvedUrl("StateLogPage.qml"), {thing: root.thing, stateType: delegate.stateType}) + } else if (delegate.eventType) { + pageStack.push(Qt.resolvedUrl("EventLogPage.qml"), {thing: root.thing, eventType: delegate.eventType}) + } else if (delegate.actionType) { + pageStack.push(Qt.resolvedUrl("ActionLogPage.qml"), {thing: root.thing, actionType: delegate.actionType}) + } } else { pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {thing: root.thing, filterTypeIds: [model.id]}) } @@ -347,10 +353,13 @@ ThingPageBase { } Timer { id: pendingTimer; interval: 1000; repeat: false; running: false } - Button { - text: actionType.displayName + Label { Layout.fillWidth: true + text: actionType.displayName + } + Button { + Layout.fillWidth: true onClicked: { if (actionDelegate.actionType.paramTypes.count === 0) { diff --git a/nymea-app/ui/devicepages/NotificationsThingPage.qml b/nymea-app/ui/devicepages/NotificationsThingPage.qml index 767fd6c4..70e3324e 100644 --- a/nymea-app/ui/devicepages/NotificationsThingPage.qml +++ b/nymea-app/ui/devicepages/NotificationsThingPage.qml @@ -265,6 +265,7 @@ ThingPageBase { engine: _engine // live: true source: "action-" + root.thing.id + "-notify" + sortOrder: Qt.DescendingOrder } delegate: BigTile { diff --git a/nymea-app/ui/devicepages/StateLogPage.qml b/nymea-app/ui/devicepages/StateLogPage.qml index 476060dd..532aee9b 100644 --- a/nymea-app/ui/devicepages/StateLogPage.qml +++ b/nymea-app/ui/devicepages/StateLogPage.qml @@ -61,6 +61,7 @@ Page { HeaderButton { imageSource: "delete" + visible: root.isLogged onClicked: { var popup = deleteLogsComponent.createObject(root) popup.open()