diff --git a/common.h b/common.h new file mode 100644 index 0000000..27efad1 --- /dev/null +++ b/common.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +using Host = QString; +using Process = QString; +using Filename = QString; +struct Logfile { + QString filename; + QString filepath; + qint64 filesize; + bool gzipCompressed; +}; +using ScanResult = QHash > >; +Q_DECLARE_METATYPE(ScanResult) + +inline bool scanResultEmpty(const ScanResult &result) +{ + return result.isEmpty() || std::all_of(result.constBegin(), result.constEnd(), [](const auto &host){ + return host.isEmpty() || std::all_of(host.constBegin(), host.constEnd(), [](const auto &process){ + return process.isEmpty(); + }); + }); +} diff --git a/dialogs/graphdialog.cpp b/dialogs/graphdialog.cpp new file mode 100644 index 0000000..8105df0 --- /dev/null +++ b/dialogs/graphdialog.cpp @@ -0,0 +1,104 @@ +#include "graphdialog.h" +#include "ui_graphdialog.h" + +#include +#include +#include +#include + +#include +#include +#include + +GraphDialog::GraphDialog(QSqlDatabase &database, QWidget *parent) : + QDialog(parent), + m_ui(std::make_unique()), + m_database(database) +{ + m_ui->setupUi(this); + + QString sql; + + if (m_database.driverName() == "QSQLITE") + { + sql = "SELECT " + "`Timestamp`, " + "COUNT(`Timestamp`) " + "FROM " + "`Logs` " + "GROUP BY " + "STRFTIME('%Y-%m-%d %H:00:00.000', `Timestamp`)"; + } + else if (m_database.driverName() == "QMYSQL") + { + sql = "SELECT " + "`Timestamp`, " + "COUNT(`Timestamp`) " + "FROM " + "`Logs` " + "GROUP BY " + "DATE_FORMAT(`Timestamp`, '%Y-%m-%d %H:%i:00.000')"; + } + else + qFatal("unknown sql driver"); + + QSqlQuery query(sql, m_database); + if (query.lastError().isValid()) + qCritical() << query.lastError().text(); + + auto chart = new QtCharts::QChart(); + chart->legend()->hide(); + chart->setTitle(tr("Charts-Test")); + + auto series = new QtCharts::QLineSeries(); + chart->addSeries(series); + + QDateTime minDt, maxDt; + int maxCount{}; + + while (query.next()) + { + const auto timestampStr = query.value(0).toString(); + qDebug() << timestampStr; + + const auto timestamp = QDateTime::fromString(timestampStr, QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")); + Q_ASSERT(timestamp.isValid()); + + if (minDt.isNull() || timestamp < minDt) + minDt = timestamp; + + if (maxDt.isNull() || timestamp > maxDt) + maxDt = timestamp; + + const auto count = query.value(1).toInt(); + + if (count > maxCount) + maxCount = count; + + qDebug() << timestamp << count; + + series->append(timestamp.toMSecsSinceEpoch(), count); + } + + qDebug() << minDt << maxDt; + + auto axisX = new QtCharts::QDateTimeAxis; + axisX->setRange(minDt, maxDt); + axisX->setTickCount(20); + axisX->setFormat("HH:mm:ss"); + axisX->setTitleText("Timestamp"); + chart->addAxis(axisX, Qt::AlignBottom); + series->attachAxis(axisX); + + auto axisY = new QtCharts::QValueAxis; + axisY->setMax(maxCount); + axisY->setLabelFormat("%i"); + axisY->setTitleText("Logs count"); + chart->addAxis(axisY, Qt::AlignLeft); + series->attachAxis(axisY); + + m_ui->chartView->setRenderHint(QPainter::Antialiasing); + m_ui->chartView->setChart(chart); +} + +GraphDialog::~GraphDialog() = default; diff --git a/dialogs/graphdialog.h b/dialogs/graphdialog.h new file mode 100644 index 0000000..1f6160e --- /dev/null +++ b/dialogs/graphdialog.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +class QSqlDatabase; + +namespace Ui { class GraphDialog; } + +class GraphDialog : public QDialog +{ + Q_OBJECT + +public: + GraphDialog(QSqlDatabase &database, QWidget *parent = nullptr); + ~GraphDialog() override; + +private: + const std::unique_ptr m_ui; + + QSqlDatabase &m_database; +}; diff --git a/dialogs/graphdialog.ui b/dialogs/graphdialog.ui new file mode 100644 index 0000000..f6aaaaf --- /dev/null +++ b/dialogs/graphdialog.ui @@ -0,0 +1,55 @@ + + + GraphDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + + + + QDialogButtonBox::Close + + + + + + + + QtCharts::QChartView + QGraphicsView +
qchartview.h
+
+
+ + + + buttonBox + rejected() + GraphDialog + close() + + + 199 + 278 + + + 199 + 149 + + + + +
diff --git a/dialogs/opendialog.cpp b/dialogs/opendialog.cpp new file mode 100644 index 0000000..086b3a1 --- /dev/null +++ b/dialogs/opendialog.cpp @@ -0,0 +1,36 @@ +#include "opendialog.h" +#include "ui_opendialog.h" + +#include +#include +#include + +OpenDialog::OpenDialog(QWidget *parent) : + QDialog(parent), + m_ui(std::make_unique()) +{ + m_ui->setupUi(this); + + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &OpenDialog::submit); +} + +OpenDialog::~OpenDialog() = default; + +QSqlDatabase OpenDialog::database() +{ + return m_database; +} + +void OpenDialog::submit() +{ + m_database = m_ui->databaseWidget->createConnection(); + + if (!m_database.open()) + { + QMessageBox::warning(this, tr("Could not open database!"), tr("Could not open database!") % "\n\n" % m_database.lastError().text()); + m_database = {}; + return; + } + + accept(); +} diff --git a/dialogs/opendialog.h b/dialogs/opendialog.h new file mode 100644 index 0000000..afa94b4 --- /dev/null +++ b/dialogs/opendialog.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include + +namespace Ui { class OpenDialog; } + +class OpenDialog : public QDialog +{ + Q_OBJECT + +public: + explicit OpenDialog(QWidget *parent = nullptr); + ~OpenDialog() override; + + QSqlDatabase database(); + +private slots: + void submit(); + +private: + const std::unique_ptr m_ui; + + QSqlDatabase m_database; +}; diff --git a/dialogs/opendialog.ui b/dialogs/opendialog.ui new file mode 100644 index 0000000..f4e8e2b --- /dev/null +++ b/dialogs/opendialog.ui @@ -0,0 +1,59 @@ + + + OpenDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + DatabaseWidget + QWidget +
widgets/databasewidget.h
+ 1 +
+
+ + + + buttonBox + rejected() + OpenDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/failed.png b/failed.png new file mode 100644 index 0000000..5efd283 Binary files /dev/null and b/failed.png differ diff --git a/gzipdevice.cpp b/gzipdevice.cpp new file mode 100644 index 0000000..700b254 --- /dev/null +++ b/gzipdevice.cpp @@ -0,0 +1,77 @@ +#include "gzipdevice.h" + +#include + +#include + +GzipDevice::GzipDevice(QFile &file, QObject *parent) : + QIODevice(parent), + m_file(file) +{ + if (!m_file.isOpen()) + throw std::runtime_error("file is not open"); + + setOpenMode(QIODevice::ReadOnly); + + // Prepare inflater status + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.next_in = Z_NULL; + + // Initialize inflater + m_result = inflateInit2(&strm, 15 + 16); + if (m_result != Z_OK) + throw std::runtime_error("could not init z_stream"); +} + +GzipDevice::~GzipDevice() +{ + inflateEnd(&strm); +} + +bool GzipDevice::isSequential() const +{ + return true; +} + +bool GzipDevice::atEnd() const +{ + return m_result == Z_STREAM_END; +} + +qint64 GzipDevice::readData(char *data, qint64 maxlen) +{ + if (strm.avail_in == 0) + { + strm.next_in = reinterpret_cast(m_readBuffer); + strm.avail_in = m_file.read(m_readBuffer, m_readBufferSize); + } + + strm.next_out = reinterpret_cast(data); + strm.avail_out = maxlen; + + m_result = inflate(&strm, Z_NO_FLUSH); + + switch (m_result) { + case Z_NEED_DICT: + throw std::runtime_error("decompression failed: Z_NEED_DICT"); + case Z_DATA_ERROR: + throw std::runtime_error("decompression failed: Z_DATA_ERROR"); + case Z_MEM_ERROR: + throw std::runtime_error("decompression failed: Z_MEM_ERROR"); + case Z_STREAM_ERROR: + throw std::runtime_error("decompression failed: Z_STREAM_ERROR"); + } + + return maxlen-strm.avail_out; +} + +qint64 GzipDevice::writeData(const char *data, qint64 len) +{ + Q_UNUSED(data) + Q_UNUSED(len) + qFatal("no writes allowed in GzipDevice!"); + return -1; +} diff --git a/gzipdevice.h b/gzipdevice.h new file mode 100644 index 0000000..05780c1 --- /dev/null +++ b/gzipdevice.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "zlib.h" + +class QFile; + +class GzipDevice : public QIODevice +{ +public: + GzipDevice(QFile &file, QObject *parent = nullptr); + ~GzipDevice() override; + + bool isSequential() const override; + bool atEnd() const override; + +protected: + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; + +private: + QFile &m_file; + static constexpr std::size_t m_readBufferSize { 32 * 1024 }; + char m_readBuffer[m_readBufferSize]; + z_stream strm; + int m_result; +}; diff --git a/loading.gif b/loading.gif new file mode 100644 index 0000000..1560b64 Binary files /dev/null and b/loading.gif differ diff --git a/loganalyzer.pro b/loganalyzer.pro new file mode 100644 index 0000000..cf8e3f6 --- /dev/null +++ b/loganalyzer.pro @@ -0,0 +1,76 @@ +QT += core gui widgets network sql charts + +TARGET = loganalyzer +TEMPLATE = app + +DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000 + +CONFIG += c++14 +QMAKE_CXXFLAGS_RELEASE -= -O1 +QMAKE_CXXFLAGS_RELEASE -= -O2 +QMAKE_CXXFLAGS_RELEASE *= -O3 +QMAKE_CXXFLAGS_RELEASE -= -march=x86-64 +QMAKE_CXXFLAGS_RELEASE -= -mtune=generic +QMAKE_CXXFLAGS_RELEASE *= -march=native +QMAKE_CXXFLAGS_RELEASE *= -mtune=native + +LIBS += -lz + +SOURCES += main.cpp \ + mainwindow.cpp \ + wizard/importwizard.cpp \ + wizard/intropage.cpp \ + wizard/databasepage.cpp \ + wizard/importtypepage.cpp \ + wizard/localimportpage.cpp \ + wizard/conclusionpage.cpp \ + wizard/remoteimportoverviewpage.cpp \ + wizard/tablespage.cpp \ + threads/tablecreatorthread.cpp \ + models/checklistmodel.cpp \ + threads/remotescannerthread.cpp \ + wizard/remoteimportscanpage.cpp \ + threads/importthread.cpp \ + wizard/importprogresspage.cpp \ + dialogs/opendialog.cpp \ + widgets/fileselectionwidget.cpp \ + widgets/databasewidget.cpp \ + gzipdevice.cpp \ + dialogs/graphdialog.cpp \ + models/sqlrelationaltablemodel.cpp + +HEADERS += \ + mainwindow.h \ + wizard/importwizard.h \ + wizard/intropage.h \ + wizard/databasepage.h \ + wizard/importtypepage.h \ + wizard/localimportpage.h \ + wizard/conclusionpage.h \ + wizard/remoteimportoverviewpage.h \ + wizard/tablespage.h \ + threads/tablecreatorthread.h \ + models/checklistmodel.h \ + threads/remotescannerthread.h \ + wizard/remoteimportscanpage.h \ + common.h \ + threads/importthread.h \ + wizard/importprogresspage.h \ + dialogs/opendialog.h \ + widgets/fileselectionwidget.h \ + widgets/databasewidget.h \ + gzipdevice.h \ + dialogs/graphdialog.h \ + models/sqlrelationaltablemodel.h + +FORMS += \ + mainwindow.ui \ + dialogs/opendialog.ui \ + widgets/fileselectionwidget.ui \ + widgets/databasewidget.ui \ + wizard/intropage.ui \ + wizard/databasepage.ui \ + dialogs/graphdialog.ui + +RESOURCES += \ + resources.qrc diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..911ee4b --- /dev/null +++ b/main.cpp @@ -0,0 +1,17 @@ +#include +#include + +#include "mainwindow.h" +#include "common.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + qRegisterMetaType(); + + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..770bb42 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,217 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wizard/importwizard.h" +#include "dialogs/opendialog.h" +#include "dialogs/graphdialog.h" +#include "models/sqlrelationaltablemodel.h" + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + m_ui(std::make_unique()) +{ + m_ui->setupUi(this); + + m_ui->actionNew->setShortcut(QKeySequence::New); + m_ui->actionOpen->setShortcut(QKeySequence::Open); + m_ui->actionQuit->setShortcut(QKeySequence::Quit); + + connect(m_ui->actionNew, &QAction::triggered, this, &MainWindow::newClicked); + connect(m_ui->actionOpen, &QAction::triggered, this, &MainWindow::openClicked); + connect(m_ui->actionClose, &QAction::triggered, this, &MainWindow::closeClicked); + connect(m_ui->actionQuit, &QAction::triggered, QCoreApplication::instance(), &QCoreApplication::quit); + connect(m_ui->actionGraph, &QAction::triggered, this, &MainWindow::graphClicked); + + for (QAction *action : { m_ui->actionTimestamp, m_ui->actionHost, m_ui->actionProcess, m_ui->actionFilename, m_ui->actionThread, m_ui->actionType, m_ui->actionMessage }) + connect(action, &QAction::toggled, this, &MainWindow::showColumns); + + connect(m_ui->lineEdit, &QLineEdit::returnPressed, this, &MainWindow::updateQuery); + connect(m_ui->pushButton, &QPushButton::pressed, this, &MainWindow::updateQuery); + + m_ui->tableView->setItemDelegate(new QSqlRelationalDelegate(m_ui->tableView)); + + connect(m_ui->tableView, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenu); +} + +MainWindow::~MainWindow() = default; + +void MainWindow::newClicked() +{ + ImportWizard wizard(this); + if (wizard.exec() == QDialog::Accepted && wizard.field("open").toBool()) + { + m_ui->actionNew->setVisible(false); + m_ui->actionOpen->setVisible(false); + m_ui->actionClose->setVisible(true); + m_ui->actionGraph->setEnabled(true); + m_ui->lineEdit->setEnabled(true); + m_ui->pushButton->setEnabled(true); + + m_database = wizard.database(); + setupModel(); + updateQuery(); + } +} + +void MainWindow::openClicked() +{ + OpenDialog dialog(this); + if (dialog.exec() == QDialog::Accepted) + { + m_ui->actionNew->setVisible(false); + m_ui->actionOpen->setVisible(false); + m_ui->actionClose->setVisible(true); + m_ui->actionGraph->setEnabled(true); + m_ui->lineEdit->setEnabled(true); + m_ui->pushButton->setEnabled(true); + + m_database = dialog.database(); + setupModel(); + updateQuery(); + } +} + +void MainWindow::closeClicked() +{ + m_ui->actionNew->setVisible(true); + m_ui->actionOpen->setVisible(true); + m_ui->actionClose->setVisible(false); + m_ui->actionGraph->setEnabled(false); + m_ui->lineEdit->setEnabled(false); + m_ui->pushButton->setEnabled(false); + + m_ui->tableView->setModel(nullptr); + m_model = nullptr; + m_database.close(); + m_database = QSqlDatabase(); +} + +void MainWindow::graphClicked() +{ + GraphDialog(m_database, this).exec(); +} + +void MainWindow::updateQuery() +{ + auto filter = m_ui->lineEdit->text(); + if (!filter.trimmed().isEmpty()) + { + filter.replace("||", "OR"); + filter.replace("&&", "AND"); + m_model->setFilter(filter); + } + + if (!m_model->select()) + QMessageBox::warning(this, tr("Query failed!"), tr("Query failed!") % "\n\n" % m_model->query().lastError().text()); +} + +void MainWindow::showColumns() +{ + for (const auto &pair : { + std::make_pair(m_ui->actionTimestamp, ColumnTimestamp), + std::make_pair(m_ui->actionHost, ColumnHost), + std::make_pair(m_ui->actionProcess, ColumnProcess), + std::make_pair(m_ui->actionFilename, ColumnFilename), + std::make_pair(m_ui->actionThread, ColumnThread), + std::make_pair(m_ui->actionType, ColumnType), + std::make_pair(m_ui->actionMessage, ColumnMessage) + }) + m_ui->tableView->setColumnHidden(std::get<1>(pair), !std::get<0>(pair)->isChecked()); +} + +void MainWindow::showContextMenu(const QPoint &pos) +{ + const auto index = m_ui->tableView->indexAt(pos); + if (!index.isValid()) + return; + + QMenu menu(this); + const auto exec = [this,&menu,&pos](){ return menu.exec(m_ui->tableView->viewport()->mapToGlobal(pos)); }; + + qDebug() << m_model->record(index.row()).value(2); + const auto data = m_model->data(index, Qt::EditRole).toString(); + + switch (index.column()) + { + case ColumnTimestamp: + { + auto minute = menu.addAction(tr("Filter by minute")); + auto second = menu.addAction(tr("Filter by second")); + auto action = exec(); + if (action == minute || action == second) + { + const auto format = QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz"); + + auto dateTime = QDateTime::fromString(data, format); + auto time = dateTime.time(); + time.setHMS(time.hour(), time.minute(), action==minute ? 0 : time.second()); + dateTime.setTime(time); + m_ui->lineEdit->setText(QString("`Timestamp` BETWEEN \"%0\" AND \"%1\"").arg(dateTime.toString(format), dateTime.addMSecs(action==minute ? 59999 : 999).toString(format))); + updateQuery(); + } + break; + } + case ColumnHost: + if (menu.addAction(tr("Filter by host")) == exec()) + { + m_ui->lineEdit->setText(QString("`Hosts`.`Name` = \"%0\"").arg(data)); + updateQuery(); + } + break; + case ColumnProcess: + if (menu.addAction(tr("Filter by process")) == exec()) + { + m_ui->lineEdit->setText(QString("`Processes`.`Name` = \"%0\"").arg(data)); + updateQuery(); + } + break; + case ColumnFilename: + if (menu.addAction(tr("Filter by filename")) == exec()) + { + m_ui->lineEdit->setText(QString("`Filenames`.`Name` = \"%0\"").arg(data)); + updateQuery(); + } + break; + case ColumnThread: + if (menu.addAction(tr("Filter by thread")) == exec()) + { + m_ui->lineEdit->setText(QString("`Threads`.`Name` = \"%0\"").arg(data)); + updateQuery(); + } + break; + case ColumnType: + if (menu.addAction(tr("Filter by type")) == exec()) + { + m_ui->lineEdit->setText(QString("`Types`.`Name` = \"%0\"").arg(data)); + updateQuery(); + } + break; + } +} + +void MainWindow::setupModel() +{ + m_ui->tableView->setModel(nullptr); + m_model = std::make_unique(this, m_database); + m_model->setTable("Logs"); + m_model->setRelation(ColumnHost, QSqlRelation("Hosts", "ID", "Name")); + m_model->setRelation(ColumnProcess, QSqlRelation("Processes", "ID", "Name")); + m_model->setRelation(ColumnFilename, QSqlRelation("Filenames", "ID", "Name")); + m_model->setRelation(ColumnThread, QSqlRelation("Threads", "ID", "Name")); + m_model->setRelation(ColumnType, QSqlRelation("Types", "ID", "Name")); + m_ui->tableView->setModel(m_model.get()); + m_ui->tableView->setColumnHidden(ColumnID, true); + showColumns(); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..22ec2ab --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include + +class QSqlRelationalTableModel; + +namespace Ui { class MainWindow; } + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private slots: + void newClicked(); + void openClicked(); + void closeClicked(); + void graphClicked(); + void updateQuery(); + void showColumns(); + void showContextMenu(const QPoint &pos); + +private: + void setupModel(); + + enum { ColumnID, ColumnTimestamp, ColumnHost, ColumnProcess, ColumnFilename, ColumnThread, ColumnType, ColumnMessage }; + + const std::unique_ptr m_ui; + + QSqlDatabase m_database; + std::unique_ptr m_model; +}; diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..d882efc --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,216 @@ + + + MainWindow + + + + 0 + 0 + 965 + 578 + + + + MainWindow + + + + + + + + + false + + + + + + + + + + false + + + Filter + + + + + + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + false + + + + + + + + + 0 + 0 + 965 + 20 + + + + + &File + + + + + + + + + + &View + + + + + + + + + + + + &Tools + + + + + + + + + + + &New project + + + + + &Open + + + + + Quit + + + + + Close Database + + + false + + + + + true + + + true + + + Timestamp + + + + + true + + + true + + + Host + + + + + true + + + true + + + Process + + + + + true + + + false + + + Filename + + + + + true + + + true + + + Thread + + + + + true + + + true + + + Type + + + + + true + + + true + + + Message + + + + + false + + + &Graph + + + + + Raw data + + + + + + + diff --git a/models/checklistmodel.cpp b/models/checklistmodel.cpp new file mode 100644 index 0000000..dc451a4 --- /dev/null +++ b/models/checklistmodel.cpp @@ -0,0 +1,141 @@ +#include "checklistmodel.h" + +ChecklistModel::ChecklistModel(QObject *parent) : + QAbstractListModel(parent) +{ +} + +ChecklistModel::ChecklistModel(const QStringList &items, QObject *parent) : + QAbstractListModel(parent) +{ + for (const auto &item : items) + m_items.append(std::make_pair(item, true)); +} + +ChecklistModel::ChecklistModel(const QList > &items, QObject *parent) : + QAbstractListModel(parent), + m_items(items) +{ +} + +QStringList ChecklistModel::items() const +{ + QStringList items; + + for (const auto &pair : m_items) + items.append(std::get<0>(pair)); + + return items; +} + +void ChecklistModel::setItems(const QStringList &items) +{ + emit beginResetModel(); + + m_items.clear(); + + for (const auto &item : items) + m_items.append(std::make_pair(item, true)); + + emit endResetModel(); +} + +void ChecklistModel::setItems(const QList > &items) +{ + emit beginResetModel(); + + m_items = items; + + emit endResetModel(); +} + +QStringList ChecklistModel::enabledItems() const +{ + QStringList items; + + for (const auto &pair : m_items) + if (std::get<1>(pair)) + items.append(std::get<0>(pair)); + + return items; +} + +QStringList ChecklistModel::disabledItems() const +{ + QStringList items; + + for (const auto &pair : m_items) + if (!std::get<1>(pair)) + items.append(std::get<0>(pair)); + + return items; +} + +int ChecklistModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return m_items.count(); +} + +QModelIndex ChecklistModel::sibling(int row, int column, const QModelIndex &idx) const +{ + if (!idx.isValid() || column != 0 || row >= m_items.count() || row < 0) + return QModelIndex(); + return createIndex(row, 0); +} + +QMap ChecklistModel::itemData(const QModelIndex &index) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) + return QMap{}; + const auto &item = m_items.at(index.row()); + return QMap{{ + std::make_pair(Qt::DisplayRole, std::get<0>(item)), + std::make_pair(Qt::EditRole, std::get<0>(item)), + std::make_pair(Qt::CheckStateRole, std::get<1>(item) ? Qt::Checked : Qt::Unchecked) + }}; +} + +bool ChecklistModel::setItemData(const QModelIndex &index, const QMap &roles) +{ + if (roles.isEmpty()) + return false; + if (std::any_of(roles.keyBegin(), roles.keyEnd(), [](int role) { return role != Qt::CheckStateRole; })) + return false; + auto roleIter = roles.constFind(Qt::CheckStateRole); + Q_ASSERT(roleIter != roles.constEnd()); + return setData(index, roleIter.value(), roleIter.key()); +} + +QVariant ChecklistModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= m_items.size()) + return QVariant(); + const auto &item = m_items.at(index.row()); + if (role == Qt::DisplayRole || role == Qt::EditRole) + return std::get<0>(item); + if (role == Qt::CheckStateRole) + return std::get<1>(item) ? Qt::Checked : Qt::Unchecked; + return QVariant(); +} + +bool ChecklistModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() >= 0 && index.row() < m_items.size() && role == Qt::CheckStateRole) + { + auto &item = m_items[index.row()]; + std::get<1>(item) = value.toBool(); + emit dataChanged(index, index, { Qt::CheckStateRole }); + return true; + } + + return false; +} + +Qt::ItemFlags ChecklistModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return QAbstractListModel::flags(index); + return QAbstractListModel::flags(index) | Qt::ItemIsUserCheckable; +} diff --git a/models/checklistmodel.h b/models/checklistmodel.h new file mode 100644 index 0000000..f75401f --- /dev/null +++ b/models/checklistmodel.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include + +class Q_CORE_EXPORT ChecklistModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit ChecklistModel(QObject *parent = nullptr); + explicit ChecklistModel(const QStringList &items, QObject *parent = nullptr); + explicit ChecklistModel(const QList > &strings, QObject *parent = nullptr); + + QStringList items() const; + void setItems(const QStringList &items); + void setItems(const QList > &items); + QStringList enabledItems() const; + QStringList disabledItems() const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QModelIndex sibling(int row, int column, const QModelIndex &idx) const override; + QMap itemData(const QModelIndex &index) const override; + bool setItemData(const QModelIndex &index, const QMap &roles) override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + +private: + QList > m_items; +}; diff --git a/models/sqlrelationaltablemodel.cpp b/models/sqlrelationaltablemodel.cpp new file mode 100644 index 0000000..ff5d3d2 --- /dev/null +++ b/models/sqlrelationaltablemodel.cpp @@ -0,0 +1,14 @@ +#include "sqlrelationaltablemodel.h" + +SqlRelationalTableModel::SqlRelationalTableModel(QObject *parent, QSqlDatabase db) : + QSqlRelationalTableModel(parent, db) +{ +} + +QVariant SqlRelationalTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) + return {}; + + return QSqlRelationalTableModel::headerData(section, orientation, role); +} diff --git a/models/sqlrelationaltablemodel.h b/models/sqlrelationaltablemodel.h new file mode 100644 index 0000000..03e7105 --- /dev/null +++ b/models/sqlrelationaltablemodel.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class SqlRelationalTableModel : public QSqlRelationalTableModel +{ + Q_OBJECT + +public: + explicit SqlRelationalTableModel(QObject *parent = nullptr, QSqlDatabase db = QSqlDatabase()); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..872bcc9 --- /dev/null +++ b/resources.qrc @@ -0,0 +1,7 @@ + + + loading.gif + failed.png + succeeded.png + + diff --git a/succeeded.png b/succeeded.png new file mode 100644 index 0000000..8b6f7a4 Binary files /dev/null and b/succeeded.png differ diff --git a/threads/importthread.cpp b/threads/importthread.cpp new file mode 100644 index 0000000..b2e4270 --- /dev/null +++ b/threads/importthread.cpp @@ -0,0 +1,353 @@ +#include "importthread.h" + +#include +#include +#include +#include + +#include + +#include "gzipdevice.h" + +ImportThread::ImportThread(QSqlDatabase &database, const ScanResult &result, const QString &timeFormat, QObject *parent) : + QThread(parent), + m_database(database), + m_result(result), + m_timeFormat(timeFormat), + m_queryInsertLog(database), + m_queryFindHost(database), + m_queryInsertHost(database), + m_queryFindProcess(database), + m_queryInsertProcess(database), + m_queryFindFilename(database), + m_queryInsertFilename(database), + m_queryFindThread(database), + m_queryInsertThread(database), + m_queryFindType(database), + m_queryInsertType(database), + m_totalSize(calculateTotalSize()) +{ +} + +void ImportThread::run() +{ + { + int query { 0 }; + typedef std::tuple Row; + for (const auto &tuple : { + Row(tr("insert log"), m_queryInsertLog, "INSERT INTO `Logs` (`Timestamp`, `HostID`, `ProcessID`, `FilenameID`, `ThreadID`, `TypeID`, `Message`) " + "VALUES (:Timestamp, :HostID, :ProcessID, :FilenameID, :ThreadID, :TypeID, :Message);"), + Row(tr("find host"), m_queryFindHost, "SELECT `ID` FROM `Hosts` WHERE `Name` = :Name;"), + Row(tr("insert host"), m_queryInsertHost, "INSERT INTO `Hosts` (`Name`) VALUES (:Name);"), + Row(tr("find host"), m_queryFindProcess, "SELECT `ID` FROM `Processes` WHERE `Name` = :Name;"), + Row(tr("insert host"), m_queryInsertProcess, "INSERT INTO `Processes` (`Name`) VALUES (:Name);"), + Row(tr("find host"), m_queryFindFilename, "SELECT `ID` FROM `Filenames` WHERE `Name` = :Name;"), + Row(tr("insert host"), m_queryInsertFilename, "INSERT INTO `Filenames` (`Name`) VALUES (:Name);"), + Row(tr("find host"), m_queryFindThread, "SELECT `ID` FROM `Threads` WHERE `Name` = :Name;"), + Row(tr("insert host"), m_queryInsertThread, "INSERT INTO `Threads` (`Name`) VALUES (:Name);"), + Row(tr("find host"), m_queryFindType, "SELECT `ID` FROM `Types` WHERE `Name` = :Name;"), + Row(tr("insert host"), m_queryInsertType, "INSERT INTO `Types` (`Name`) VALUES (:Name);") + }) + { + if (isInterruptionRequested()) + return; + + emit statusUpdate(tr("Preparing query to %0...").arg(std::get<0>(tuple))); + if (!std::get<1>(tuple).prepare(std::get<2>(tuple))) + { + emit statusUpdate(tr("Failed.")); + emit logMessage(tr("Could not prepare query to %0: %1").arg(std::get<0>(tuple), std::get<1>(tuple).lastError().text())); + return; + } + emit progressUpdate(query++, 11); + } + } + + qint64 processedSize { 0 }; + + for (auto hostsIter = m_result.constBegin(); hostsIter != m_result.constEnd(); hostsIter++) + { + if (isInterruptionRequested()) + return; + + m_queryInsertLog.bindValue(":HostID", getHostID(hostsIter.key())); + + for (auto processesIter = hostsIter.value().constBegin(); processesIter != hostsIter.value().constEnd(); processesIter++) + { + if (isInterruptionRequested()) + return; + + m_queryInsertLog.bindValue(":ProcessID", getProcessID(processesIter.key())); + + for (auto datesIter = processesIter.value().constBegin(); datesIter != processesIter.value().constEnd(); datesIter++) + { + if (isInterruptionRequested()) + return; + + m_queryInsertLog.bindValue(":FilenameID", getFilenameID(datesIter.value().filename)); + + emit logMessage(datesIter.value().filename); + + QFile file(datesIter.value().filepath); + QFile::OpenMode flags = QIODevice::ReadOnly; + if (!datesIter.value().gzipCompressed) + flags |= QIODevice::Text; + if (!file.open(flags)) + { + emit logMessage(tr("Could not open logfile: %0").arg(file.errorString())); + continue; + } + + struct { + QDateTime dateTime; + QString thread { "main" }; + QString type; + QString message; + } test; + + const auto insert = [&test,this,&processedSize,&file](){ + m_queryInsertLog.bindValue(":Timestamp", test.dateTime.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz"))); + m_queryInsertLog.bindValue(":ThreadID", getThreadID(test.thread)); + m_queryInsertLog.bindValue(":TypeID", getTypeID(test.type)); + m_queryInsertLog.bindValue(":Message", test.message); + if (!m_queryInsertLog.exec()) + emit logMessage(tr("could not execute query to insert log: %0").arg(m_queryInsertLog.lastError().text())); + + m_logsInserted++; + const auto now = QDateTime::currentDateTime(); + if (m_lastProgressUpdate.isNull() || m_lastProgressUpdate.msecsTo(now) >= 100) + { + emit statusUpdate(tr("%0 logs inserted...").arg(m_logsInserted)); + emit progressUpdate(processedSize + file.pos(), m_totalSize); + m_lastProgressUpdate = now; + } + }; + + int indentionOffset; + + if (!m_database.transaction()) + { + emit statusUpdate(tr("Aborted.")); + emit logMessage(tr("Could not start transaction: %0").arg(m_database.lastError().text())); + return; + } + + try + { + const std::unique_ptr gzipProxy([&datesIter,&file](){ + if (datesIter.value().gzipCompressed) + return std::make_unique(file); + return std::unique_ptr{}; + }()); + + QTextStream textStream(gzipProxy != nullptr ? static_cast(gzipProxy.get()) : &file); + while(!textStream.atEnd()) + { + if (isInterruptionRequested()) + { + if (!m_database.rollback()) + { + emit statusUpdate(tr("Aborted.")); + emit logMessage(tr("Could not rollback transaction: %0").arg(m_database.lastError().text())); + } + return; + } + + auto line = textStream.readLine(); + const auto match = m_lineRegex.match(line); + if (match.hasMatch()) + { + if (!test.dateTime.isNull()) + insert(); + + test.message = match.captured(4); + + { + const auto threadMatch = m_threadRegex.match(test.message); + if (threadMatch.hasMatch()) + test.thread = threadMatch.captured(1); + } + + test.dateTime = { datesIter.key(), QTime::fromString(match.captured(2), m_timeFormat) }; + + test.type = match.captured(3); + test.type = test.type.left(test.type.indexOf(':')); + + indentionOffset = match.captured(1).length(); + } + else + { + if (!test.dateTime.isNull()) + { + if (line.length() >= indentionOffset && + std::all_of(line.constBegin(), line.constBegin() + indentionOffset, [](const QChar &c){ return c == ' '; })) + line.remove(0, indentionOffset); + + test.message.append("\n"); + test.message.append(line); + } + } + } + + if (!test.dateTime.isNull()) + insert(); + } + catch (const std::exception &ex) + { + emit logMessage(tr("Aborted: %0").arg(ex.what())); + } + + if (!m_database.commit()) + { + emit statusUpdate(tr("Aborted.")); + emit logMessage(tr("Could not commit transaction: %0").arg(m_database.lastError().text())); + return; + } + + processedSize += file.size(); + emit statusUpdate(tr("%0 logs inserted...").arg(m_logsInserted)); + emit progressUpdate(processedSize, m_totalSize); + } + } + } +} + +int ImportThread::getHostID(const QString &host) +{ + const auto iter = m_hosts.find(host); + if (iter != m_hosts.constEnd()) + return *iter; + + m_queryFindHost.bindValue(":Name", host); + if (!m_queryFindHost.exec()) + qFatal("could not execute query to find host: %s", qPrintable(m_queryFindHost.lastError().text())); + + if (m_queryFindHost.next()) + { + const auto id = m_queryFindHost.value(0).toInt(); + m_hosts.insert(host, id); + return id; + } + + m_queryInsertHost.bindValue(":Name", host); + if (!m_queryInsertHost.exec()) + qFatal("could not execute query to insert host: %s", qPrintable(m_queryInsertHost.lastError().text())); + + const auto id = m_queryInsertHost.lastInsertId().toInt(); + m_hosts.insert(host, id); + return id; +} + +int ImportThread::getProcessID(const QString &process) +{ + const auto iter = m_processes.find(process); + if (iter != m_processes.constEnd()) + return *iter; + + m_queryFindProcess.bindValue(":Name", process); + if (!m_queryFindProcess.exec()) + qFatal("could not execute query to find process: %s", qPrintable(m_queryFindProcess.lastError().text())); + + if (m_queryFindProcess.next()) + { + const auto id = m_queryFindProcess.value(0).toInt(); + m_processes.insert(process, id); + return id; + } + + m_queryInsertProcess.bindValue(":Name", process); + if (!m_queryInsertProcess.exec()) + qFatal("could not execute query to insert process: %s", qPrintable(m_queryInsertProcess.lastError().text())); + + const auto id = m_queryInsertProcess.lastInsertId().toInt(); + m_processes.insert(process, id); + return id; +} + +int ImportThread::getFilenameID(const QString &filename) +{ + const auto iter = m_filenames.find(filename); + if (iter != m_filenames.constEnd()) + return *iter; + + m_queryFindFilename.bindValue(":Name", filename); + if (!m_queryFindFilename.exec()) + qFatal("could not execute query to find filename: %s", qPrintable(m_queryFindFilename.lastError().text())); + + if (m_queryFindFilename.next()) + { + const auto id = m_queryFindFilename.value(0).toInt(); + m_filenames.insert(filename, id); + return id; + } + + m_queryInsertFilename.bindValue(":Name", filename); + if (!m_queryInsertFilename.exec()) + qFatal("could not execute query to insert filename: %s", qPrintable(m_queryInsertFilename.lastError().text())); + + const auto id = m_queryInsertFilename.lastInsertId().toInt(); + m_filenames.insert(filename, id); + return id; +} + +int ImportThread::getThreadID(const QString &thread) +{ + const auto iter = m_threads.find(thread); + if (iter != m_threads.constEnd()) + return *iter; + + m_queryFindThread.bindValue(":Name", thread); + if (!m_queryFindThread.exec()) + qFatal("could not execute query to find thread: %s", qPrintable(m_queryFindThread.lastError().text())); + + if (m_queryFindThread.next()) + { + const auto id = m_queryFindThread.value(0).toInt(); + m_threads.insert(thread, id); + return id; + } + + m_queryInsertThread.bindValue(":Name", thread); + if (!m_queryInsertThread.exec()) + qFatal("could not execute query to insert thread: %s", qPrintable(m_queryInsertThread.lastError().text())); + + const auto id = m_queryInsertThread.lastInsertId().toInt(); + m_threads.insert(thread, id); + return id; +} + +int ImportThread::getTypeID(const QString &type) +{ + const auto iter = m_types.find(type); + if (iter != m_types.constEnd()) + return *iter; + + m_queryFindType.bindValue(":Name", type); + if (!m_queryFindType.exec()) + qFatal("could not execute query to find type: %s", qPrintable(m_queryFindType.lastError().text())); + + if (m_queryFindType.next()) + { + const auto id = m_queryFindType.value(0).toInt(); + m_types.insert(type, id); + return id; + } + + m_queryInsertType.bindValue(":Name", type); + if (!m_queryInsertType.exec()) + qFatal("could not execute query to insert type: %s", qPrintable(m_queryInsertType.lastError().text())); + + const auto id = m_queryInsertType.lastInsertId().toInt(); + m_types.insert(type, id); + return id; +} + +qint64 ImportThread::calculateTotalSize() const +{ + qint64 totalSize { 0 }; + for (auto hostsIter = m_result.constBegin(); hostsIter != m_result.constEnd(); hostsIter++) + for (auto processesIter = hostsIter.value().constBegin(); processesIter != hostsIter.value().constEnd(); processesIter++) + for (auto datesIter = processesIter.value().constBegin(); datesIter != processesIter.value().constEnd(); datesIter++) + totalSize += datesIter.value().filesize; + return totalSize; +} diff --git a/threads/importthread.h b/threads/importthread.h new file mode 100644 index 0000000..d60d5ac --- /dev/null +++ b/threads/importthread.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include "common.h" + +class QSqlDatabase; + +class ImportThread : public QThread +{ + Q_OBJECT + +public: + ImportThread(QSqlDatabase &database, const ScanResult &result, const QString &timeFormat, QObject *parent = nullptr); + +signals: + void statusUpdate(const QString &message); + void progressUpdate(qint64 finished, qint64 total); + void logMessage(const QString &message); + +protected: + void run() override; + +private: + int getHostID(const QString &host); + int getProcessID(const QString &process); + int getFilenameID(const QString &filename); + int getThreadID(const QString &thread); + int getTypeID(const QString &type); + + qint64 calculateTotalSize() const; + + QSqlDatabase &m_database; + ScanResult m_result; + QString m_timeFormat; + + QHash m_hosts; + QHash m_processes; + QHash m_filenames; + QHash m_threads; + QHash m_types; + + QSqlQuery m_queryInsertLog; + QSqlQuery m_queryFindHost; + QSqlQuery m_queryInsertHost; + QSqlQuery m_queryFindProcess; + QSqlQuery m_queryInsertProcess; + QSqlQuery m_queryFindFilename; + QSqlQuery m_queryInsertFilename; + QSqlQuery m_queryFindThread; + QSqlQuery m_queryInsertThread; + QSqlQuery m_queryFindType; + QSqlQuery m_queryInsertType; + + const qint64 m_totalSize; + quint64 m_logsInserted { 0 }; + QDateTime m_lastProgressUpdate; + + const QRegularExpression m_lineRegex { "^(([0-9]+:[0-9]+:[0-9]+(?:\\.[0-9]+)?) (FATAL: |ERROR: |WARNING: |INFO: |DEBUG: |DEBUG1: |DEBUG2: ))(.*)$" }; + const QRegularExpression m_threadRegex { "----- Thread context: (.*) -----$" }; +}; diff --git a/threads/remotescannerthread.cpp b/threads/remotescannerthread.cpp new file mode 100644 index 0000000..7d9453a --- /dev/null +++ b/threads/remotescannerthread.cpp @@ -0,0 +1,86 @@ +#include "remotescannerthread.h" + +#include +#include +#include + +RemoteScannerThread::RemoteScannerThread(const QString &dir, QObject *parent) : + QThread(parent), + m_dir(dir) +{ +} + +void RemoteScannerThread::run() +{ + scanForHosts(); +} + +void RemoteScannerThread::scanForHosts() +{ + if (m_files) + qFatal("thread was already run"); + + QDirIterator iter(m_dir, QDir::Dirs | QDir::NoDotAndDotDot); + while (iter.hasNext()) + { + if (isInterruptionRequested()) + return; + + const QFileInfo fileInfo(iter.next()); + + const auto host = fileInfo.fileName(); + + emit logMessage(tr("Scanning host %0...").arg(host)); + + scanForLogfiles(host, fileInfo.absoluteFilePath()); + } + + emit progressUpdate(m_files, m_files - m_valid); + emit finished(m_result); +} + +void RemoteScannerThread::scanForLogfiles(const QString &hostname, const QString &hostDir) +{ + auto &hostEntry = m_result[hostname]; + + QDirIterator dirIter(hostDir, { "*.log.gz" }, QDir::Files); + while (dirIter.hasNext()) + { + if (isInterruptionRequested()) + return; + + const QFileInfo fileInfo(dirIter.next()); + + m_files++; + + const auto match = m_fileExpression.match(fileInfo.fileName()); + if (!match.hasMatch()) + continue; + + const auto process = match.captured(1); + if (process.endsWith(QStringLiteral(".olog"))) + continue; + + const auto date = QDate::fromString(match.captured(2), QStringLiteral("yyyyMMdd")); + if (date.isNull()) + continue; + + m_valid++; + + Q_ASSERT(!hostEntry[process].contains(date)); + + hostEntry[process].insert(date, { + hostname % "/" % fileInfo.fileName(), + fileInfo.absoluteFilePath(), + fileInfo.size(), + true + }); + + const auto now = QDateTime::currentDateTime(); + if (m_lastUpdate.isNull() || m_lastUpdate.msecsTo(now) >= 100) + { + emit progressUpdate(m_files, m_files - m_valid); + m_lastUpdate = now; + } + } +} diff --git a/threads/remotescannerthread.h b/threads/remotescannerthread.h new file mode 100644 index 0000000..c0191ae --- /dev/null +++ b/threads/remotescannerthread.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "common.h" + +class RemoteScannerThread : public QThread +{ + Q_OBJECT + + const QRegularExpression m_fileExpression { "^(.+)-([0-9]{8})\\.log?" }; + +public: + RemoteScannerThread(const QString &dir, QObject *parent = nullptr); + +signals: + void progressUpdate(int totalFiles, int skippedFiles); + void logMessage(const QString &message); + void finished(const ScanResult &result); + +protected: + void run() override; + +private: + void scanForHosts(); + void scanForLogfiles(const QString &hostname, const QString &hostDir); + + QString m_dir; + + ScanResult m_result; + + int m_files { 0 }; + int m_valid { 0 }; + + QDateTime m_lastUpdate; +}; diff --git a/threads/tablecreatorthread.cpp b/threads/tablecreatorthread.cpp new file mode 100644 index 0000000..f09ac93 --- /dev/null +++ b/threads/tablecreatorthread.cpp @@ -0,0 +1,101 @@ +#include "tablecreatorthread.h" + +#include +#include +#include +#include + +const QStringList TableCreatorThread::m_tables { "Hosts", "Processes", "Filenames", "Threads", "Types", "Logs" }; + +TableCreatorThread::TableCreatorThread(QSqlDatabase &database, QObject *parent) : + QThread(parent), m_database(database) +{ +} + +const QStringList &TableCreatorThread::tables() +{ + return m_tables; +} + +void TableCreatorThread::run() +{ + int index { 0 }; + + for (const QString tableName : m_tables) + { + if (isInterruptionRequested()) + return; + + const auto sql = [&tableName,type=m_database.driverName()]() -> QString { + if (tableName == "Logs") + { + if (type == "QSQLITE") + { + return "CREATE TABLE IF NOT EXISTS `Logs` (" + "`ID` INTEGER PRIMARY KEY, " + "`Timestamp` TEXT NOT NULL, " + "`HostID` INTEGER NOT NULL, " + "`ProcessID` INTEGER NOT NULL, " + "`FilenameID` INTEGER NOT NULL, " + "`ThreadID` INTEGER NOT NULL, " + "`TypeID` INTEGER NOT NULL, " + "`Message` TEXT NOT NULL, " + "FOREIGN KEY (`HostID`) REFERENCES `Hosts`(`ID`), " + "FOREIGN KEY (`ProcessID`) REFERENCES `Processes`(`ID`), " + "FOREIGN KEY (`FilenameID`) REFERENCES `Filenames`(`ID`), " + "FOREIGN KEY (`ThreadID`) REFERENCES `Threads`(`ID`), " + "FOREIGN KEY (`TypeID`) REFERENCES `Types`(`ID`)" + ");"; + } + else if (type == "QMYSQL") + { + return "CREATE TABLE IF NOT EXISTS `Logs` (" + "`ID` INT UNSIGNED NOT NULL AUTO_INCREMENT, " + "`Timestamp` DATETIME NOT NULL, " + "`HostID` INT UNSIGNED NOT NULL, " + "`ProcessID` INT UNSIGNED NOT NULL, " + "`FilenameID` INT UNSIGNED NOT NULL, " + "`ThreadID` INT UNSIGNED NOT NULL, " + "`TypeID` INT UNSIGNED NOT NULL, " + "`Message` LONGTEXT NOT NULL, " + "PRIMARY KEY(`ID`), " + "INDEX(`Timestamp`), " + "FOREIGN KEY (`HostID`) REFERENCES `Hosts`(`ID`), " + "FOREIGN KEY (`ProcessID`) REFERENCES `Processes`(`ID`), " + "FOREIGN KEY (`FilenameID`) REFERENCES `Filenames`(`ID`), " + "FOREIGN KEY (`ThreadID`) REFERENCES `Threads`(`ID`), " + "FOREIGN KEY (`TypeID`) REFERENCES `Types`(`ID`)" + ");"; + } + } + else + { + if (type == "QSQLITE") + { + return QString("CREATE TABLE IF NOT EXISTS `%0` (" + "`ID` INTEGER PRIMARY KEY, " + "`Name` TEXT NOT NULL UNIQUE" + ");").arg(tableName); + } + else if (type == "QMYSQL") + { + return QString("CREATE TABLE IF NOT EXISTS `%0` (" + "`ID` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, " + "`Name` VARCHAR(255) NOT NULL UNIQUE" + ");").arg(tableName); + } + } + + qFatal("unknown database type %s", qPrintable(type)); + }(); + + QSqlQuery query(sql, m_database); + if (query.lastError().isValid()) + { + qCritical() << query.lastError().text(); + return; + } + + emit someSignal(index++); + } +} diff --git a/threads/tablecreatorthread.h b/threads/tablecreatorthread.h new file mode 100644 index 0000000..92c4548 --- /dev/null +++ b/threads/tablecreatorthread.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +class QSqlDatabase; + +class TableCreatorThread : public QThread +{ + Q_OBJECT + + static const QStringList m_tables; + +public: + explicit TableCreatorThread(QSqlDatabase &database, QObject *parent = nullptr); + + static const QStringList &tables(); + +signals: + void someSignal(int index); + +protected: + void run() override; + +private: + QSqlDatabase &m_database; +}; diff --git a/widgets/databasewidget.cpp b/widgets/databasewidget.cpp new file mode 100644 index 0000000..bf7eae1 --- /dev/null +++ b/widgets/databasewidget.cpp @@ -0,0 +1,104 @@ +#include "databasewidget.h" +#include "ui_databasewidget.h" + +DatabaseWidget::DatabaseWidget(QWidget *parent) : + QWidget(parent), + m_ui(std::make_unique()) +{ + m_ui->setupUi(this); + + m_ui->comboBox->addItem(tr("SQLite"), "QSQLITE"); + m_ui->comboBox->addItem(tr("MySQL"), "QMYSQL"); + + // for debugging + setDriver("QMYSQL"); +// setMysqlHostname("sql7.freemysqlhosting.net"); +// setMysqlUsername("sql7285815"); +// setMysqlPassword("BKhysrtqKl"); +// setMysqlDatabase("sql7285815"); + + //setMysqlHostname("brunner.ninja"); + setMysqlHostname("localhost"); + setMysqlUsername("logtest"); + setMysqlPassword("logtest"); + setMysqlDatabase("logtest"); +} + +DatabaseWidget::~DatabaseWidget() = default; + +QString DatabaseWidget::driver() const +{ + return m_ui->comboBox->currentData().toString(); +} + +void DatabaseWidget::setDriver(const QString &driver) +{ + m_ui->comboBox->setCurrentIndex(m_ui->comboBox->findData(driver)); +} + +QString DatabaseWidget::sqliteFilepath() const +{ + return m_ui->fileSelectionWidget->path(); +} + +void DatabaseWidget::setSqliteFilepath(const QString &sqliteFilepath) +{ + m_ui->fileSelectionWidget->setPath(sqliteFilepath); +} + +QString DatabaseWidget::mysqlHostname() const +{ + return m_ui->lineEditHostname->text(); +} + +void DatabaseWidget::setMysqlHostname(const QString &mysqlHostname) +{ + m_ui->lineEditHostname->setText(mysqlHostname); +} + +QString DatabaseWidget::mysqlUsername() const +{ + return m_ui->lineEditUsername->text(); +} + +void DatabaseWidget::setMysqlUsername(const QString &mysqlUsername) +{ + m_ui->lineEditUsername->setText(mysqlUsername); +} + +QString DatabaseWidget::mysqlPassword() const +{ + return m_ui->lineEditPassword->text(); +} + +void DatabaseWidget::setMysqlPassword(const QString &mysqlPassword) +{ + m_ui->lineEditPassword->setText(mysqlPassword); +} + +QString DatabaseWidget::mysqlDatabase() const +{ + return m_ui->lineEditDatabase->text(); +} + +void DatabaseWidget::setMysqlDatabase(const QString &mysqlDatabase) +{ + m_ui->lineEditDatabase->setText(mysqlDatabase); +} + +QSqlDatabase DatabaseWidget::createConnection(const QString &connectionName) +{ + auto db = QSqlDatabase::addDatabase(driver(), connectionName); + + if (db.driverName() == "QSQLITE") + db.setDatabaseName(sqliteFilepath()); + else + { + db.setHostName(mysqlHostname()); + db.setUserName(mysqlUsername()); + db.setPassword(mysqlPassword()); + db.setDatabaseName(mysqlDatabase()); + } + + return db; +} diff --git a/widgets/databasewidget.h b/widgets/databasewidget.h new file mode 100644 index 0000000..59b7dd5 --- /dev/null +++ b/widgets/databasewidget.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +namespace Ui { class DatabaseWidget; } + +class DatabaseWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DatabaseWidget(QWidget *parent = nullptr); + ~DatabaseWidget() override; + + QString driver() const; + void setDriver(const QString &driver); + + QString sqliteFilepath() const; + void setSqliteFilepath(const QString &sqliteFilepath); + + QString mysqlHostname() const; + void setMysqlHostname(const QString &mysqlHostname); + + QString mysqlUsername() const; + void setMysqlUsername(const QString &mysqlUsername); + + QString mysqlPassword() const; + void setMysqlPassword(const QString &mysqlPassword); + + QString mysqlDatabase() const; + void setMysqlDatabase(const QString &mysqlDatabase); + + QSqlDatabase createConnection(const QString& connectionName = QLatin1String(QSqlDatabase::defaultConnection)); + +private: + const std::unique_ptr m_ui; +}; diff --git a/widgets/databasewidget.ui b/widgets/databasewidget.ui new file mode 100644 index 0000000..d9e9e4f --- /dev/null +++ b/widgets/databasewidget.ui @@ -0,0 +1,124 @@ + + + DatabaseWidget + + + + 0 + 0 + 400 + 175 + + + + Form + + + + + + + + + 0 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + <b>Hostname:</b> + + + + + + + <b>Username:</b> + + + + + + + <b>Password:</b> + + + + + + + <b>Database:</b> + + + + + + + QLineEdit::Password + + + + + + + + + + + + + + + FileSelectionWidget + QWidget +
widgets/fileselectionwidget.h
+ 1 +
+
+ + + + comboBox + currentIndexChanged(int) + stackedWidget + setCurrentIndex(int) + + + 199 + 20 + + + 199 + 101 + + + + +
diff --git a/widgets/fileselectionwidget.cpp b/widgets/fileselectionwidget.cpp new file mode 100644 index 0000000..e67f6aa --- /dev/null +++ b/widgets/fileselectionwidget.cpp @@ -0,0 +1,75 @@ +#include "fileselectionwidget.h" +#include "ui_fileselectionwidget.h" + +#include + +FileSelectionWidget::FileSelectionWidget(QWidget *parent) : + QWidget(parent), + m_ui(std::make_unique()), + m_mode(Mode::OpenFile) +{ + m_ui->setupUi(this); + + connect(m_ui->lineEdit, &QLineEdit::textChanged, this, &FileSelectionWidget::pathChanged); + connect(m_ui->pushButton, &QAbstractButton::pressed, this, &FileSelectionWidget::selectPath); +} + +FileSelectionWidget::FileSelectionWidget(const Mode mode, QWidget *parent) : + QWidget(parent), + m_ui(std::make_unique()), + m_mode(mode) +{ + m_ui->setupUi(this); + + connect(m_ui->lineEdit, &QLineEdit::textChanged, this, &FileSelectionWidget::pathChanged); + connect(m_ui->pushButton, &QAbstractButton::pressed, this, &FileSelectionWidget::selectPath); +} + +FileSelectionWidget::FileSelectionWidget(const Mode mode, const QString &path, QWidget *parent) : + QWidget(parent), + m_ui(std::make_unique()), + m_mode(mode) +{ + m_ui->setupUi(this); + + m_ui->lineEdit->setText(path); + + connect(m_ui->lineEdit, &QLineEdit::textChanged, this, &FileSelectionWidget::pathChanged); + connect(m_ui->pushButton, &QAbstractButton::pressed, this, &FileSelectionWidget::selectPath); +} + +FileSelectionWidget::~FileSelectionWidget() = default; + +FileSelectionWidget::Mode FileSelectionWidget::mode() const +{ + return m_mode; +} + +void FileSelectionWidget::setMode(const FileSelectionWidget::Mode mode) +{ + m_mode = mode; +} + +QString FileSelectionWidget::path() const +{ + return m_ui->lineEdit->text(); +} + +void FileSelectionWidget::setPath(const QString &path) +{ + m_ui->lineEdit->setText(path); +} + +void FileSelectionWidget::selectPath() +{ + QString path; + switch (m_mode) + { + case Mode::OpenFile: path = QFileDialog::getOpenFileName(this); break; + case Mode::SaveFile: path = QFileDialog::getSaveFileName(this); break; + case Mode::ExistingDirectory: path = QFileDialog::getExistingDirectory(this); break; + } + + if (!path.isEmpty()) + m_ui->lineEdit->setText(path); +} diff --git a/widgets/fileselectionwidget.h b/widgets/fileselectionwidget.h new file mode 100644 index 0000000..320fbfa --- /dev/null +++ b/widgets/fileselectionwidget.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +namespace Ui { class FileSelectionWidget; } + +class FileSelectionWidget : public QWidget +{ + Q_OBJECT + +public: + enum class Mode { + OpenFile, SaveFile, ExistingDirectory + }; + + explicit FileSelectionWidget(QWidget *parent = nullptr); + FileSelectionWidget(const Mode mode, QWidget *parent = nullptr); + FileSelectionWidget(const Mode mode, const QString &path, QWidget *parent = nullptr); + ~FileSelectionWidget() override; + + Mode mode() const; + void setMode(const Mode mode); + + QString path() const; + void setPath(const QString &path); + +signals: + void pathChanged(const QString &path); + +private slots: + void selectPath(); + +private: + const std::unique_ptr m_ui; + Mode m_mode; +}; diff --git a/widgets/fileselectionwidget.ui b/widgets/fileselectionwidget.ui new file mode 100644 index 0000000..bf2b6e5 --- /dev/null +++ b/widgets/fileselectionwidget.ui @@ -0,0 +1,31 @@ + + + FileSelectionWidget + + + + 0 + 0 + 400 + 41 + + + + Form + + + + + + + + + Select... + + + + + + + + diff --git a/wizard/conclusionpage.cpp b/wizard/conclusionpage.cpp new file mode 100644 index 0000000..6634487 --- /dev/null +++ b/wizard/conclusionpage.cpp @@ -0,0 +1,54 @@ +#include "conclusionpage.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importwizard.h" + +ConclusionPage::ConclusionPage(QWidget *parent) : + QWizardPage(parent) +{ + setTitle(tr("Conclusion")); + setSubTitle(tr("Import successfully finished!")); + + auto layout = new QVBoxLayout; + + m_label = new QLabel; + layout->addWidget(m_label); + + layout->addStretch(1); + + m_checkBox = new QCheckBox(tr("Open new database")); + m_checkBox->setChecked(true); + layout->addWidget(m_checkBox); + registerField("open", m_checkBox); + + layout->addStretch(1); + + setLayout(layout); +} + +void ConclusionPage::initializePage() +{ + auto importWizard = qobject_cast(wizard()); + Q_ASSERT(importWizard); + Q_ASSERT(importWizard->database().isOpen()); + + QSqlQuery query("SELECT COUNT(*) FROM `Logs`;", importWizard->database()); + if (query.lastError().isValid()) + { + QMessageBox::warning(nullptr, tr("Could not get count!"), tr("Could not get count!") % "\n\n" % query.lastError().text()); + return; + } + + const auto fetched = query.next(); + Q_ASSERT(fetched); + + m_label->setText(tr("%0 rows have been imported.").arg(query.value(0).toInt())); +} diff --git a/wizard/conclusionpage.h b/wizard/conclusionpage.h new file mode 100644 index 0000000..3a9e215 --- /dev/null +++ b/wizard/conclusionpage.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +class QLabel; +class QCheckBox; + +class ConclusionPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ConclusionPage(QWidget *parent = nullptr); + + void initializePage() override; + +private: + QLabel *m_label; + QCheckBox *m_checkBox; +}; diff --git a/wizard/databasepage.cpp b/wizard/databasepage.cpp new file mode 100644 index 0000000..5ffb0d0 --- /dev/null +++ b/wizard/databasepage.cpp @@ -0,0 +1,46 @@ +#include "databasepage.h" +#include "ui_databasepage.h" + +#include +#include +#include +#include +#include +#include + +#include "widgets/databasewidget.h" +#include "importwizard.h" + +DatabasePage::DatabasePage(QWidget *parent) : + QWizardPage(parent), + m_ui(std::make_unique()) +{ + m_ui->setupUi(this); + + setCommitPage(true); +} + +DatabasePage::~DatabasePage() = default; + +int DatabasePage::nextId() const +{ + return int(ImportWizard::Pages::Tables); +} + +bool DatabasePage::validatePage() +{ + auto importWizard = qobject_cast(wizard()); + Q_ASSERT(importWizard); + Q_ASSERT(!importWizard->database().isOpen()); + + importWizard->database() = m_ui->databaseWidget->createConnection(); + + if (!importWizard->database().open()) + { + QMessageBox::warning(this, tr("Could not open database!"), tr("Could not open database!") % "\n\n" % importWizard->database().lastError().text()); + importWizard->database() = {}; + return false; + } + + return true; +} diff --git a/wizard/databasepage.h b/wizard/databasepage.h new file mode 100644 index 0000000..5459b4f --- /dev/null +++ b/wizard/databasepage.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +namespace Ui { class DatabasePage; } + +class DatabasePage : public QWizardPage +{ + Q_OBJECT + +public: + explicit DatabasePage(QWidget *parent = nullptr); + ~DatabasePage() override; + + int nextId() const override; + + bool validatePage() override; + +private: + const std::unique_ptr m_ui; +}; diff --git a/wizard/databasepage.ui b/wizard/databasepage.ui new file mode 100644 index 0000000..9e7024f --- /dev/null +++ b/wizard/databasepage.ui @@ -0,0 +1,51 @@ + + + DatabasePage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Database + + + Please setup the database connection. + + + + + + + + + Qt::Vertical + + + + 20 + 263 + + + + + + + + + DatabaseWidget + QWidget +
widgets/databasewidget.h
+ 1 +
+
+ + +
diff --git a/wizard/importprogresspage.cpp b/wizard/importprogresspage.cpp new file mode 100644 index 0000000..1f601ba --- /dev/null +++ b/wizard/importprogresspage.cpp @@ -0,0 +1,117 @@ +#include "importprogresspage.h" + +#include +#include +#include +#include +#include + +#include "importwizard.h" +#include "threads/importthread.h" + +ImportProgressPage::ImportProgressPage(QWidget *parent) : + QWizardPage(parent) +{ + setTitle(tr("Import Progress")); + setSubTitle(tr("TODO...")); + + auto layout = new QVBoxLayout; + + { + auto hboxLayout = new QHBoxLayout; + + m_labelIcon = new QLabel; + hboxLayout->addWidget(m_labelIcon); + + m_labelStatus = new QLabel; + hboxLayout->addWidget(m_labelStatus, 1); + + layout->addLayout(hboxLayout); + } + + m_progressBar = new QProgressBar; + m_progressBar->setMaximum(100); + layout->addWidget(m_progressBar); + + m_logView = new QPlainTextEdit; + m_logView->setReadOnly(true); + layout->addWidget(m_logView, 1); + + setLayout(layout); +} + +ImportProgressPage::~ImportProgressPage() = default; + +void ImportProgressPage::initializePage() +{ + auto importWizard = qobject_cast(wizard()); + Q_ASSERT(importWizard); + Q_ASSERT(importWizard->database().isOpen()); + + const auto result = wizard()->property("result").value(); + const auto timeFormat = wizard()->property("timeFormat").toString(); + + m_logView->clear(); + + m_thread = std::make_unique(importWizard->database(), result, timeFormat, this); + connect(m_thread.get(), &ImportThread::statusUpdate, this, &ImportProgressPage::statusUpdate); + connect(m_thread.get(), &ImportThread::progressUpdate, this, &ImportProgressPage::progressUpdate); + connect(m_thread.get(), &ImportThread::logMessage, this, &ImportProgressPage::logMessage); + connect(m_thread.get(), &QThread::finished, this, &ImportProgressPage::finished); + m_thread->start(); + + m_labelIcon->setMovie(&m_movieLoading); + m_movieLoading.start(); +} + +void ImportProgressPage::cleanupPage() +{ + if (m_thread) + { + m_thread->requestInterruption(); + m_thread->wait(); + m_thread = nullptr; + } + m_movieLoading.stop(); +} + +int ImportProgressPage::nextId() const +{ + return int(ImportWizard::Pages::Conclusion); +} + +bool ImportProgressPage::isComplete() const +{ + return m_thread == nullptr; +} + +void ImportProgressPage::statusUpdate(const QString &message) +{ + m_labelStatus->setText(message); +} + +void ImportProgressPage::progressUpdate(qint64 finished, qint64 total) +{ + while (total & 0xFFFFFFFF00000000) + { + finished = finished >> 8; + total = total >> 8; + } + + m_progressBar->setMaximum(total); + m_progressBar->setValue(finished); +} + +void ImportProgressPage::logMessage(const QString &message) +{ + m_logView->appendHtml(QString("%0: %1
").arg(QTime::currentTime().toString(), message)); +} + +void ImportProgressPage::finished() +{ + cleanupPage(); + emit completeChanged(); + + m_labelIcon->setPixmap(m_pixmapSucceeded); + logMessage(tr("Finished.")); +} diff --git a/wizard/importprogresspage.h b/wizard/importprogresspage.h new file mode 100644 index 0000000..3c52fb0 --- /dev/null +++ b/wizard/importprogresspage.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include + +class QLabel; +class QProgressBar; +class QPlainTextEdit; + +class ImportThread; + +class ImportProgressPage : public QWizardPage +{ + Q_OBJECT + + const QPixmap m_pixmapSucceeded { ":/loganalyzer/succeeded.png" }; + const QPixmap m_pixmapFailed { ":/loganalyzer/failed.png" }; + QMovie m_movieLoading { ":/loganalyzer/loading.gif" }; + +public: + explicit ImportProgressPage(QWidget *parent = nullptr); + ~ImportProgressPage() override; + + void initializePage() override; + void cleanupPage() override; + + int nextId() const override; + bool isComplete() const override; + +private slots: + void statusUpdate(const QString &message); + void progressUpdate(qint64 finished, qint64 total); + void logMessage(const QString &message); + void finished(); + +private: + QLabel *m_labelIcon; + QLabel *m_labelStatus; + QProgressBar *m_progressBar; + QPlainTextEdit *m_logView; + + std::unique_ptr m_thread; +}; diff --git a/wizard/importtypepage.cpp b/wizard/importtypepage.cpp new file mode 100644 index 0000000..9a5df1d --- /dev/null +++ b/wizard/importtypepage.cpp @@ -0,0 +1,117 @@ +#include "importtypepage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importwizard.h" +#include "common.h" + +ImportTypePage::ImportTypePage(QWidget *parent) : + QWizardPage(parent) +{ + setTitle(tr("Import type")); + setSubTitle(tr("Please select which type of log files you would like to import.")); + + auto layout = new QVBoxLayout; + + m_radioLocal = new QRadioButton(tr("Local: Typically found under /tmp/testfw_log/tests")); + m_radioLocal->setChecked(true); + layout->addWidget(m_radioLocal); + + m_radioRemote = new QRadioButton(tr("Remote: Typically found under /log or /log2")); + layout->addWidget(m_radioRemote); + + layout->addStretch(1); + + { + auto hboxLayout = new QHBoxLayout; + + m_lineEdit = new QLineEdit; + hboxLayout->addWidget(m_lineEdit, 1); + registerField("folder", m_lineEdit); + + { + auto toolButton = new QToolButton; + toolButton->setText(tr("Select...")); + connect(toolButton, &QAbstractButton::pressed, this, &ImportTypePage::selectFolder); + hboxLayout->addWidget(toolButton); + } + + layout->addLayout(hboxLayout); + } + + layout->addStretch(1); + + setLayout(layout); +} + +int ImportTypePage::nextId() const +{ + if (m_radioLocal->isChecked()) + return int(ImportWizard::Pages::LocalImport); + if (m_radioRemote->isChecked()) + return int(ImportWizard::Pages::RemoteImportScan); + Q_UNREACHABLE(); +} + +bool ImportTypePage::validatePage() +{ + if (m_lineEdit->text().isEmpty()) + { + QMessageBox::warning(this, tr("No logfolder defined!"), tr("No logfolder defined!")); + return false; + } + + QDir dir(m_lineEdit->text()); + if (!dir.exists()) + { + QMessageBox::warning(this, tr("Could not find logfolder!"), tr("Could not find logfolder!")); + return false; + } + + if (m_radioLocal->isChecked()) + { + ScanResult result; + auto &host = result["__dummyHost"]; + + for (const auto &fileInfo : dir.entryInfoList({ "*.log" }, QDir::Files)) + { + if (fileInfo.baseName().endsWith("_console")) + continue; + + host[fileInfo.baseName()][QDate()] = { + fileInfo.fileName(), + fileInfo.absoluteFilePath(), + fileInfo.size(), + false + }; + } + + if (host.isEmpty()) + { + QMessageBox::warning(this, tr("Could not find any logs!"), tr("Could not find any logs!")); + return false; + } + + wizard()->setProperty("result", QVariant::fromValue(result)); + } + + if (m_radioRemote->isChecked()) + wizard()->setProperty("folder", dir.absolutePath()); + + return true; +} + +void ImportTypePage::selectFolder() +{ + const auto path = QFileDialog::getExistingDirectory(this, tr("Select log folder")); + if (!path.isEmpty()) + m_lineEdit->setText(path); +} diff --git a/wizard/importtypepage.h b/wizard/importtypepage.h new file mode 100644 index 0000000..b2014e1 --- /dev/null +++ b/wizard/importtypepage.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class QRadioButton; +class QLineEdit; + +class ImportTypePage : public QWizardPage +{ + Q_OBJECT + +public: + explicit ImportTypePage(QWidget *parent = nullptr); + + int nextId() const override; + + bool validatePage() override; + +private slots: + void selectFolder(); + +private: + QRadioButton *m_radioLocal; + QRadioButton *m_radioRemote; + + QLineEdit *m_lineEdit; +}; diff --git a/wizard/importwizard.cpp b/wizard/importwizard.cpp new file mode 100644 index 0000000..f6339a8 --- /dev/null +++ b/wizard/importwizard.cpp @@ -0,0 +1,35 @@ +#include "importwizard.h" + +#include "intropage.h" +#include "databasepage.h" +#include "tablespage.h" +#include "importtypepage.h" +#include "localimportpage.h" +#include "remoteimportscanpage.h" +#include "remoteimportoverviewpage.h" +#include "importprogresspage.h" +#include "conclusionpage.h" + +ImportWizard::ImportWizard(QWidget *parent, Qt::WindowFlags flags) : + QWizard(parent, flags) +{ + setPage(int(Pages::Introduction), new IntroPage); + setPage(int(Pages::Database), new DatabasePage); + setPage(int(Pages::Tables), new TablesPage); + setPage(int(Pages::ImportType), new ImportTypePage); + setPage(int(Pages::LocalImport), new LocalImportPage); + setPage(int(Pages::RemoteImportScan), new RemoteImportScanPage); + setPage(int(Pages::RemoteImportOverview), new RemoteImportOverviewPage); + setPage(int(Pages::ImportProgress), new ImportProgressPage); + setPage(int(Pages::Conclusion), new ConclusionPage); +} + +QSqlDatabase &ImportWizard::database() +{ + return m_database; +} + +const QSqlDatabase &ImportWizard::database() const +{ + return m_database; +} diff --git a/wizard/importwizard.h b/wizard/importwizard.h new file mode 100644 index 0000000..4eaa4ef --- /dev/null +++ b/wizard/importwizard.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +class ImportWizard : public QWizard +{ + Q_OBJECT + +public: + enum class Pages { Introduction, Database, Tables, ImportType, LocalImport, RemoteImportScan, RemoteImportOverview, ImportProgress, Conclusion }; + + ImportWizard(QWidget *parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags()); + + QSqlDatabase &database(); + const QSqlDatabase &database() const; + +private: + QSqlDatabase m_database; +}; diff --git a/wizard/intropage.cpp b/wizard/intropage.cpp new file mode 100644 index 0000000..157975d --- /dev/null +++ b/wizard/intropage.cpp @@ -0,0 +1,18 @@ +#include "intropage.h" +#include "ui_intropage.h" + +#include "importwizard.h" + +IntroPage::IntroPage(QWidget *parent) : + QWizardPage(parent), + m_ui(std::make_unique()) +{ + m_ui->setupUi(this); +} + +IntroPage::~IntroPage() = default; + +int IntroPage::nextId() const +{ + return int(ImportWizard::Pages::Database); +} diff --git a/wizard/intropage.h b/wizard/intropage.h new file mode 100644 index 0000000..851c23d --- /dev/null +++ b/wizard/intropage.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include + +namespace Ui { class IntroPage; } + +class IntroPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit IntroPage(QWidget *parent = nullptr); + ~IntroPage() override; + + int nextId() const override; + +private: + const std::unique_ptr m_ui; +}; diff --git a/wizard/intropage.ui b/wizard/intropage.ui new file mode 100644 index 0000000..045bcac --- /dev/null +++ b/wizard/intropage.ui @@ -0,0 +1,34 @@ + + + IntroPage + + + + 0 + 0 + 400 + 300 + + + + WizardPage + + + Introduction + + + TODO... + + + + + + TODO: long introduction... + + + + + + + + diff --git a/wizard/localimportpage.cpp b/wizard/localimportpage.cpp new file mode 100644 index 0000000..b7448ca --- /dev/null +++ b/wizard/localimportpage.cpp @@ -0,0 +1,161 @@ +#include "localimportpage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "importwizard.h" + +LocalImportPage::LocalImportPage(QWidget *parent) : + QWizardPage(parent) +{ + setTitle(tr("Local Import")); + setSubTitle(tr("TODO...")); + setCommitPage(true); + + connect(&m_model, &ChecklistModel::dataChanged, this, &LocalImportPage::updateSummary); + + auto layout = new QVBoxLayout; + + { + auto hboxLayout = new QHBoxLayout; + + { + auto formLayout = new QFormLayout; + + m_lineEditHost = new QLineEdit(QHostInfo::localHostName()); + formLayout->addRow(tr("Host:"), m_lineEditHost); + + m_dateEdit = new QDateEdit(QDate::currentDate()); + formLayout->addRow(tr("Date:"), m_dateEdit); + + m_comboBox = new QComboBox; + m_comboBox->addItem(tr("Without milliseconds"), "HH:mm:ss"); + m_comboBox->addItem(tr("With milliseconds"), "HH:mm:ss.zzz"); + formLayout->addRow(tr("Timestamp:"), m_comboBox); + + hboxLayout->addLayout(formLayout); + } + + { + auto view = new QListView; + view->setModel(&m_model); + hboxLayout->addWidget(view, 1); + } + + layout->addLayout(hboxLayout, 1); + } + + m_labelSummary = new QLabel; + layout->addWidget(m_labelSummary); + + setLayout(layout); +} + +int LocalImportPage::nextId() const +{ + return int(ImportWizard::Pages::ImportProgress); +} + +void LocalImportPage::initializePage() +{ + m_result = wizard()->property("result").value(); + + Q_ASSERT(m_result.count() == 1); + + { + auto processes = m_result.values().first().keys(); + processes.sort(); + m_model.setItems(processes); + } + + updateSummary(); +} + +bool LocalImportPage::validatePage() +{ + auto result = filterResult(m_result); + + if (scanResultEmpty(result)) + { + QMessageBox::warning(this, tr("No files to import!"), tr("No files to import!")); + return false; + } + + Q_ASSERT(result.count() == 1); + + auto host = result.values().first(); + for (auto iter = host.begin(); iter != host.end(); iter++) + { + auto &dates = iter.value(); + Q_ASSERT(dates.count() == 1); + + const auto logfile = dates.values().first(); + dates.clear(); + dates.insert(m_dateEdit->date(), logfile); + } + + result.clear(); + result.insert(m_lineEditHost->text(), host); + + wizard()->setProperty("result", QVariant::fromValue(result)); + wizard()->setProperty("timeFormat", m_comboBox->currentData().toString()); + + return true; +} + +void LocalImportPage::updateSummary() +{ + if (m_result.isEmpty()) + return; + + const auto result = filterResult(m_result); + + int logFiles { 0 }; + qint64 totalSize { 0 }; + + for (auto hostsIter = result.constBegin(); hostsIter != result.constEnd(); hostsIter++) + for (auto processesIter = hostsIter.value().constBegin(); processesIter != hostsIter.value().constEnd(); processesIter++) + for (auto datesIter = processesIter.value().constBegin(); datesIter != processesIter.value().constEnd(); datesIter++) + { + logFiles++; + totalSize += datesIter.value().filesize; + } + + QString sizeStr; + for (const QString prefix : { "K", "M", "G", "T" }) + { + if (totalSize > 1024) + { + totalSize /= 1024; + sizeStr = QString::number(totalSize) % prefix; + } + } + + m_labelSummary->setText(tr("Filters match %0 files (%1B)").arg(logFiles).arg(sizeStr)); +} + +ScanResult LocalImportPage::filterResult(ScanResult result) const +{ + const auto processes = m_model.enabledItems().toSet(); + + for (auto hostsIter = result.begin(); hostsIter != result.end(); hostsIter++) + { + for (auto processesIter = hostsIter.value().begin(); processesIter != hostsIter.value().end(); ) + { + if (processes.contains(processesIter.key())) + processesIter++; + else + processesIter = hostsIter.value().erase(processesIter); + } + } + + return result; +} diff --git a/wizard/localimportpage.h b/wizard/localimportpage.h new file mode 100644 index 0000000..cd1718f --- /dev/null +++ b/wizard/localimportpage.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "models/checklistmodel.h" +#include "common.h" + +class QLineEdit; +class QDateEdit; +class QComboBox; +class QLabel; + +class LocalImportPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit LocalImportPage(QWidget *parent = nullptr); + + int nextId() const override; + + void initializePage() override; + bool validatePage() override; + +private slots: + void updateSummary(); + +private: + ScanResult filterResult(ScanResult result) const; + + ScanResult m_result; + + ChecklistModel m_model; + + QLineEdit *m_lineEditHost; + QDateEdit *m_dateEdit; + QComboBox *m_comboBox; + QLabel *m_labelSummary; +}; diff --git a/wizard/remoteimportoverviewpage.cpp b/wizard/remoteimportoverviewpage.cpp new file mode 100644 index 0000000..d45619b --- /dev/null +++ b/wizard/remoteimportoverviewpage.cpp @@ -0,0 +1,248 @@ +#include "remoteimportoverviewpage.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "importwizard.h" +#include "common.h" + +RemoteImportOverviewPage::RemoteImportOverviewPage(QWidget *parent) : + QWizardPage(parent) +{ + setTitle(tr("Remote Import Overview")); + setSubTitle(tr("TODO....")); + setCommitPage(true); + + connect(&m_modelHosts, &ChecklistModel::dataChanged, this, &RemoteImportOverviewPage::updateSummary); + connect(&m_modelProcesses, &ChecklistModel::dataChanged, this, &RemoteImportOverviewPage::updateSummary); + + auto layout = new QVBoxLayout; + + { + auto groupBox = new QGroupBox(tr("Date-Range:")); + + auto hboxLayout = new QHBoxLayout; + + { + auto label = new QLabel(tr("From:")); + hboxLayout->addWidget(label); + } + + m_dateEditFrom = new QDateEdit; + connect(m_dateEditFrom, &QDateTimeEdit::dateChanged, this, &RemoteImportOverviewPage::updateSummary); + hboxLayout->addWidget(m_dateEditFrom); + + hboxLayout->addStretch(1); + + { + auto label = new QLabel(tr("To:")); + hboxLayout->addWidget(label); + } + + m_dateEditTo = new QDateEdit; + connect(m_dateEditTo, &QDateTimeEdit::dateChanged, this, &RemoteImportOverviewPage::updateSummary); + hboxLayout->addWidget(m_dateEditTo); + + hboxLayout->addStretch(1); + + { + auto label = new QLabel(tr("Timestamp:")); + hboxLayout->addWidget(label); + } + + m_comboBox = new QComboBox; + m_comboBox->addItem(tr("Without milliseconds"), "HH:mm:ss"); + m_comboBox->addItem(tr("With milliseconds"), "HH:mm:ss.zzz"); + hboxLayout->addWidget(m_comboBox); + + groupBox->setLayout(hboxLayout); + + layout->addWidget(groupBox); + } + + { + auto hboxLayout = new QHBoxLayout; + + { + auto vboxLayout = new QVBoxLayout; + + { + auto label = new QLabel(tr("Hosts:")); + vboxLayout->addWidget(label); + } + + { + auto view = new QListView; + view->setModel(&m_modelHosts); + vboxLayout->addWidget(view, 1); + } + + hboxLayout->addLayout(vboxLayout); + } + + { + auto vboxLayout = new QVBoxLayout; + + { + auto label = new QLabel(tr("Processes:")); + vboxLayout->addWidget(label); + } + + { + auto view = new QListView; + view->setModel(&m_modelProcesses); + vboxLayout->addWidget(view, 1); + } + + hboxLayout->addLayout(vboxLayout); + } + + layout->addLayout(hboxLayout, 1); + } + + m_labelSummary = new QLabel; + layout->addWidget(m_labelSummary); + + setLayout(layout); +} + +void RemoteImportOverviewPage::initializePage() +{ + m_result = wizard()->property("result").value(); + + QDate minDate, maxDate; + QSet processes; + + for (auto hostsIter = m_result.constBegin(); hostsIter != m_result.constEnd(); hostsIter++) + for (auto processesIter = hostsIter.value().constBegin(); processesIter != hostsIter.value().constEnd(); processesIter++) + { + processes.insert(processesIter.key()); + + for (auto datesIter = processesIter.value().constBegin(); datesIter != processesIter.value().constEnd(); datesIter++) + { + if (minDate.isNull() || datesIter.key() < minDate) + minDate = datesIter.key(); + if (maxDate.isNull() || datesIter.key() > maxDate) + maxDate = datesIter.key(); + } + } + + m_dateEditFrom->setDate(minDate); + m_dateEditTo->setDate(maxDate); + + { + auto hosts = m_result.keys(); + hosts.sort(); + m_modelHosts.setItems(hosts); + } + + { + auto processesList = processes.toList(); + processesList.sort(); + m_modelProcesses.setItems(processesList); + } + + updateSummary(); +} + +int RemoteImportOverviewPage::nextId() const +{ + return int(ImportWizard::Pages::ImportProgress); +} + +bool RemoteImportOverviewPage::validatePage() +{ + const auto result = filterResult(m_result); + + if (scanResultEmpty(result)) + { + QMessageBox::warning(this, tr("No files to import!"), tr("No files to import!")); + return false; + } + + wizard()->setProperty("result", QVariant::fromValue(result)); + wizard()->setProperty("timeFormat", m_comboBox->currentData().toString()); + + return true; +} + +void RemoteImportOverviewPage::updateSummary() +{ + if (m_result.isEmpty()) + return; + + const auto result = filterResult(m_result); + + int logFiles { 0 }; + qint64 totalSize { 0 }; + + for (auto hostsIter = result.constBegin(); hostsIter != result.constEnd(); hostsIter++) + for (auto processesIter = hostsIter.value().constBegin(); processesIter != hostsIter.value().constEnd(); processesIter++) + for (auto datesIter = processesIter.value().constBegin(); datesIter != processesIter.value().constEnd(); datesIter++) + { + logFiles++; + totalSize += datesIter.value().filesize; + } + + QString sizeStr; + for (const QString prefix : { "K", "M", "G", "T" }) + { + if (totalSize > 1024) + { + totalSize /= 1024; + sizeStr = QString::number(totalSize) % prefix; + } + } + + m_labelSummary->setText(tr("Filters match %0 files (%1B)").arg(logFiles).arg(sizeStr)); +} + +ScanResult RemoteImportOverviewPage::filterResult(ScanResult result) const +{ + const auto hosts = m_modelHosts.enabledItems().toSet(); + const auto processes = m_modelProcesses.enabledItems().toSet(); + + for (auto hostsIter = result.begin(); hostsIter != result.end(); ) + { + if (hosts.contains(hostsIter.key())) + { + for (auto processesIter = hostsIter.value().begin(); processesIter != hostsIter.value().end(); ) + { + if (processes.contains(processesIter.key())) + { + for (auto datesIter = processesIter.value().begin(); datesIter != processesIter.value().end(); ) + { + if (datesIter.key() >= m_dateEditFrom->date() && datesIter.key() <= m_dateEditTo->date()) + datesIter++; + else + datesIter = processesIter.value().erase(datesIter); + } + + processesIter++; + } + else + processesIter = hostsIter.value().erase(processesIter); + } + + hostsIter++; + } + else + hostsIter = result.erase(hostsIter); + } + + return result; +} diff --git a/wizard/remoteimportoverviewpage.h b/wizard/remoteimportoverviewpage.h new file mode 100644 index 0000000..e61833a --- /dev/null +++ b/wizard/remoteimportoverviewpage.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "models/checklistmodel.h" +#include "common.h" + +class QDateEdit; +class QComboBox; +class QLabel; + +class RemoteImportOverviewPage : public QWizardPage +{ + Q_OBJECT + +public: + explicit RemoteImportOverviewPage(QWidget *parent = nullptr); + + void initializePage() override; + + int nextId() const override; + + bool validatePage() override; + +private slots: + void updateSummary(); + +private: + ScanResult filterResult(ScanResult result) const; + + QDateEdit *m_dateEditFrom; + QDateEdit *m_dateEditTo; + QComboBox *m_comboBox; + QLabel *m_labelSummary; + + ChecklistModel m_modelHosts; + ChecklistModel m_modelProcesses; + + ScanResult m_result; +}; diff --git a/wizard/remoteimportscanpage.cpp b/wizard/remoteimportscanpage.cpp new file mode 100644 index 0000000..6c0b981 --- /dev/null +++ b/wizard/remoteimportscanpage.cpp @@ -0,0 +1,111 @@ +#include "remoteimportscanpage.h" + +#include +#include +#include +#include + +#include "importwizard.h" +#include "threads/remotescannerthread.h" + +RemoteImportScanPage::RemoteImportScanPage(QWidget *parent) : + QWizardPage(parent) +{ + setTitle(tr("Remote Import Scan")); + setSubTitle(tr("TODO...")); + + auto layout = new QVBoxLayout; + + { + auto hboxLayout = new QHBoxLayout; + + m_labelAnimation = new QLabel; + hboxLayout->addWidget(m_labelAnimation); + + m_labelStatus = new QLabel; + hboxLayout->addWidget(m_labelStatus, 1); + + layout->addLayout(hboxLayout); + } + + m_logView = new QPlainTextEdit; + m_logView->setReadOnly(true); + + layout->addWidget(m_logView, 1); + + setLayout(layout); +} + +RemoteImportScanPage::~RemoteImportScanPage() = default; + +int RemoteImportScanPage::nextId() const +{ + return int(ImportWizard::Pages::RemoteImportOverview); +} + +void RemoteImportScanPage::initializePage() +{ + wizard()->setProperty("result", QVariant::fromValue(ScanResult())); + + m_labelAnimation->setMovie(&m_movieLoading); + m_movieLoading.start(); + + m_logView->clear(); + + m_thread = std::make_unique(field("folder").toString(), this); + + connect(m_thread.get(), &RemoteScannerThread::progressUpdate, this, &RemoteImportScanPage::progressUpdate); + connect(m_thread.get(), &RemoteScannerThread::logMessage, this, &RemoteImportScanPage::logMessage); + connect(m_thread.get(), &RemoteScannerThread::finished, this, &RemoteImportScanPage::finished); + + m_thread->start(); +} + +void RemoteImportScanPage::cleanupPage() +{ + if (m_thread) + { + m_thread->requestInterruption(); + m_thread->wait(); + m_thread = nullptr; + } + m_movieLoading.stop(); +} + +bool RemoteImportScanPage::isComplete() const +{ + return m_thread == nullptr && !scanResultEmpty(wizard()->property("result").value()); +} + +void RemoteImportScanPage::progressUpdate(int totalFiles, int skippedFiles) +{ + m_labelStatus->setText(tr("%0 files scanned... (%1 files skipped)").arg(totalFiles).arg(skippedFiles)); +} + +void RemoteImportScanPage::logMessage(const QString &message) +{ + m_logView->appendHtml(QString("%0: %1
").arg(QTime::currentTime().toString(), message)); +} + +void RemoteImportScanPage::finished(const ScanResult &result) +{ + m_labelAnimation->setMovie(nullptr); + + const auto success = !scanResultEmpty(result); + if (success) + { + logMessage(tr("Finished")); + m_labelAnimation->setPixmap(m_pixmapSucceeded); + + wizard()->setProperty("result", QVariant::fromValue(result)); + } + else + { + logMessage(tr("Scan failed.")); + m_labelAnimation->setPixmap(m_pixmapFailed); + } + + cleanupPage(); + if (success) + emit completeChanged(); +} diff --git a/wizard/remoteimportscanpage.h b/wizard/remoteimportscanpage.h new file mode 100644 index 0000000..b3f06a6 --- /dev/null +++ b/wizard/remoteimportscanpage.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include + +#include "common.h" + +class QLabel; +class QPlainTextEdit; + +class RemoteScannerThread; + +class RemoteImportScanPage : public QWizardPage +{ + Q_OBJECT + + const QPixmap m_pixmapSucceeded { ":/loganalyzer/succeeded.png" }; + const QPixmap m_pixmapFailed { ":/loganalyzer/failed.png" }; + QMovie m_movieLoading { ":/loganalyzer/loading.gif" }; + +public: + explicit RemoteImportScanPage(QWidget *parent = nullptr); + ~RemoteImportScanPage() override; + + int nextId() const override; + + void initializePage() override; + void cleanupPage() override; + bool isComplete() const override; + +private slots: + void progressUpdate(int totalFiles, int skippedFiles); + void logMessage(const QString &message); + void finished(const ScanResult &result); + +private: + std::unique_ptr m_thread; + + QLabel *m_labelAnimation; + QLabel *m_labelStatus; + QPlainTextEdit *m_logView; +}; diff --git a/wizard/tablespage.cpp b/wizard/tablespage.cpp new file mode 100644 index 0000000..092f2aa --- /dev/null +++ b/wizard/tablespage.cpp @@ -0,0 +1,89 @@ +#include "tablespage.h" + +#include +#include + +#include "importwizard.h" +#include "threads/tablecreatorthread.h" + +TablesPage::TablesPage(QWidget *parent) : + QWizardPage(parent) +{ + setTitle(tr("Tables")); + setSubTitle(tr("TODO...")); + + auto layout = new QGridLayout; + + m_statusLabels.resize(TableCreatorThread::tables().size()); + + int index { 0 }; + for (const QString &tableName : TableCreatorThread::tables()) + { + m_statusLabels[index] = new QLabel; + layout->addWidget(m_statusLabels[index], index, 0); + + auto label = new QLabel(tr("Create table %0").arg(tableName)); + label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + layout->addWidget(label, index++, 1); + } + + setLayout(layout); +} + +TablesPage::~TablesPage() = default; + +int TablesPage::nextId() const +{ + return int(ImportWizard::Pages::ImportType); +} + +void TablesPage::initializePage() +{ + auto importWizard = qobject_cast(wizard()); + Q_ASSERT(importWizard); + Q_ASSERT(importWizard->database().isOpen()); + + for (auto label : m_statusLabels) + { + label->setMovie(nullptr); + label->setPixmap({}); + } + + m_thread = std::make_unique(importWizard->database(), this); + connect(m_thread.get(), &TableCreatorThread::someSignal, this, &TablesPage::someSlot); + + m_thread->start(); + m_statusLabels[0]->setMovie(&m_movieLoading); + m_movieLoading.start(); +} + +void TablesPage::cleanupPage() +{ + if (m_thread) + { + m_thread->requestInterruption(); + m_thread->wait(); + m_thread = nullptr; + } + m_movieLoading.stop(); +} + +bool TablesPage::isComplete() const +{ + return m_thread == nullptr; +} + +void TablesPage::someSlot(int index) +{ + Q_ASSERT(index < m_statusLabels.size()); + + m_statusLabels[index]->setMovie(nullptr); + m_statusLabels[index]->setPixmap(m_pixmapSucceeded); + if (index < m_statusLabels.size() - 1) + m_statusLabels[index+1]->setMovie(&m_movieLoading); + else + { + cleanupPage(); + emit completeChanged(); + } +} diff --git a/wizard/tablespage.h b/wizard/tablespage.h new file mode 100644 index 0000000..1d46928 --- /dev/null +++ b/wizard/tablespage.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +#include + +class QLabel; +class QSqlDatabase; + +class TableCreatorThread; + +class TablesPage : public QWizardPage +{ + Q_OBJECT + + const QPixmap m_pixmapSucceeded { ":/loganalyzer/succeeded.png" }; + const QPixmap m_pixmapFailed { ":/loganalyzer/failed.png" }; + QMovie m_movieLoading { ":/loganalyzer/loading.gif" }; + +public: + explicit TablesPage(QWidget *parent = nullptr); + ~TablesPage() override; + + int nextId() const override; + + void initializePage() override; + void cleanupPage() override; + bool isComplete() const override; + +private slots: + void someSlot(int index); + +private: + QVector m_statusLabels; + + std::unique_ptr m_thread; +};