Merge PR #1011: Improve new logging stuff further

pull/1014/head
jenkins 2023-04-25 00:01:05 +02:00
commit 83a3cbcff1
12 changed files with 443 additions and 25 deletions

View File

@ -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();
}
}

View File

@ -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 &timestamp) const;
@ -107,11 +115,15 @@ signals:
void sampleRateChanged();
void sortOrderChanged();
void liveChanged();
void fetchBlockSizeChanged();
void entriesAdded(int index, const QList<NewLogEntry*> &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;

View File

@ -973,12 +973,14 @@ Thing* ThingManager::unpackThing(ThingManager *thingManager, const QVariantMap &
loggedEventTypeIds.append(uuid.toUuid());
}
thing->setLoggedEventTypeIds(loggedEventTypeIds);
QList<QUuid> 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;

View File

@ -244,6 +244,19 @@ void Thing::setLoggedEventTypeIds(const QList<QUuid> &loggedEventTypeIds)
}
}
QList<QUuid> Thing::loggedActionTypeIds() const
{
return m_loggedActionTypeIds;
}
void Thing::setLoggedActionTypeIds(const QList<QUuid> &loggedActionTypeIds)
{
if (m_loggedActionTypeIds != loggedActionTypeIds) {
m_loggedActionTypeIds = loggedActionTypeIds;
emit loggedActionTypeIdsChanged();
}
}
int Thing::executeAction(const QString &actionName, const QVariantList &params)
{
ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName);

View File

@ -57,6 +57,7 @@ class Thing : public QObject
Q_PROPERTY(ThingClass *thingClass READ thingClass CONSTANT)
Q_PROPERTY(QList<QUuid> loggedStateTypeIds READ loggedStateTypeIds NOTIFY loggedStateTypeIdsChanged)
Q_PROPERTY(QList<QUuid> loggedEventTypeIds READ loggedEventTypeIds NOTIFY loggedEventTypeIdsChanged)
Q_PROPERTY(QList<QUuid> loggedActionTypeIds READ loggedActionTypeIds NOTIFY loggedActionTypeIdsChanged)
public:
enum ThingSetupStatus {
@ -130,6 +131,9 @@ public:
QList<QUuid> loggedEventTypeIds() const;
void setLoggedEventTypeIds(const QList<QUuid> &loggedEventTypeIds);
QList<QUuid> loggedActionTypeIds() const;
void setLoggedActionTypeIds(const QList<QUuid> &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 &params);
signals:
@ -168,6 +173,7 @@ protected:
ThingClass *m_thingClass = nullptr;
QList<QUuid> m_loggedStateTypeIds;
QList<QUuid> m_loggedEventTypeIds;
QList<QUuid> m_loggedActionTypeIds;
QList<int> m_pendingActions;
};

View File

@ -312,5 +312,7 @@
<file>ui/customviews/StateChart.qml</file>
<file>ui/devicepages/ThingLogPage.qml</file>
<file>ui/devicepages/NotificationsThingPage.qml</file>
<file>ui/devicepages/ActionLogPage.qml</file>
<file>ui/devicepages/EventLogPage.qml</file>
</qresource>
</RCC>

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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("<br>")
}
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)
}
}
}

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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("<br>")
}
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)
}
}
}

View File

@ -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) {

View File

@ -265,6 +265,7 @@ ThingPageBase {
engine: _engine
// live: true
source: "action-" + root.thing.id + "-notify"
sortOrder: Qt.DescendingOrder
}
delegate: BigTile {

View File

@ -61,6 +61,7 @@ Page {
HeaderButton {
imageSource: "delete"
visible: root.isLogged
onClicked: {
var popup = deleteLogsComponent.createObject(root)
popup.open()