diff --git a/run-clang-format.sh b/run-clang-format.sh index 35c851d..ebc37e6 100755 --- a/run-clang-format.sh +++ b/run-clang-format.sh @@ -1 +1 @@ -find . \( -iname "*.h" -o -iname "*.cpp" \) -not -path "./3rdparty/*" -not -path "./cmake-build-debug/*" -not -path "./cmake-build-release/*" -not -path "./cmake-build-debug-clang/*" | xargs clang-format -i +find . \( -iname "*.h" -o -iname "*.cpp" \) -not -path "./3rdparty/*" -not -path "./cmake-build-debug/*" -not -path "./cmake-build-release/*" -not -path "./cmake-build-debug-clang/*" -not -path "./cmake-build-release-clang/*" | xargs clang-format -i diff --git a/src/components/CMakeLists.txt b/src/components/CMakeLists.txt index e812f0a..9522dd3 100644 --- a/src/components/CMakeLists.txt +++ b/src/components/CMakeLists.txt @@ -19,7 +19,9 @@ add_library(components AvroOption.cpp AvroOption.h HexView.cpp HexView.h ExportImportFabric.cpp ExportImportFabric.h - RecordsExporter.cpp RecordsExporter.h) + RecordsExporter.cpp RecordsExporter.h + ConsumerModel.cpp ConsumerModel.h +) target_compile_definitions(components PRIVATE $<$,$>:QT_QML_DEBUG>) diff --git a/src/components/Cluster.cpp b/src/components/Cluster.cpp index fe7e6e3..d09d1d1 100644 --- a/src/components/Cluster.cpp +++ b/src/components/Cluster.cpp @@ -6,6 +6,7 @@ Cluster::Cluster(QObject *parent) : QObject(parent) , m_brokerModel(new BrokerModel(this)) , m_topicModel(new TopicModel(this)) + , m_consumerModel(new ConsumerModel(this)) {} ClusterConfig Cluster::broker() const @@ -24,6 +25,7 @@ void Cluster::setBroker(const ClusterConfig &broker) m_brokerModel->setConfig(m_broker); m_topicModel->setConfig(m_broker); + m_consumerModel->setConfig(m_broker); } BrokerModel *Cluster::brokerModel() @@ -34,4 +36,9 @@ BrokerModel *Cluster::brokerModel() TopicModel *Cluster::topicModel() { return m_topicModel; +} + +ConsumerModel *Cluster::consumerModel() +{ + return m_consumerModel; } \ No newline at end of file diff --git a/src/components/Cluster.h b/src/components/Cluster.h index 2d4432b..f39c79d 100644 --- a/src/components/Cluster.h +++ b/src/components/Cluster.h @@ -4,6 +4,7 @@ #include "BrokerModel.h" #include "ClusterConfig.h" +#include "ConsumerModel.h" #include "TopicModel.h" /**! @@ -22,6 +23,7 @@ class Cluster : public QObject Q_INVOKABLE BrokerModel *brokerModel(); Q_INVOKABLE TopicModel *topicModel(); + Q_INVOKABLE ConsumerModel *consumerModel(); signals: @@ -31,4 +33,5 @@ class Cluster : public QObject ClusterConfig m_broker; BrokerModel *m_brokerModel; TopicModel *m_topicModel; + ConsumerModel *m_consumerModel; }; diff --git a/src/components/ConsumerModel.cpp b/src/components/ConsumerModel.cpp new file mode 100644 index 0000000..08fdfd4 --- /dev/null +++ b/src/components/ConsumerModel.cpp @@ -0,0 +1,424 @@ +#include +#include + +#include "ConsumerModel.h" +#include "KafkaAdmin.h" + +namespace { + +QString groupStateToString(ConsumerGroupInfo::State state) +{ + switch (state) { + case ConsumerGroupInfo::State::Stable: + return QLatin1String("Active"); + case ConsumerGroupInfo::State::Empty: + return QLatin1String("Empty"); + case ConsumerGroupInfo::State::Rebalance: + return QLatin1String("Rebalance"); + case ConsumerGroupInfo::State::Dead: + return QLatin1String("Dead"); + default: + return QLatin1String("Unknown state"); + } +} + +QVector convertGroup(std::vector &groups) +{ + static QHash string2state = { + {"Dead", ConsumerGroupInfo::State::Dead}, + {"Empty", ConsumerGroupInfo::State::Empty}, + {"Rebalance", ConsumerGroupInfo::State::Rebalance}, + {"Stable", ConsumerGroupInfo::State::Stable}, + }; + QVector out; + out.reserve(groups.size()); + + for (auto &group : groups) { + if (group.protocolType != "consumer") { + continue; + } + + ConsumerGroupInfo info; + info.group = QString::fromStdString(group.group); + info.state = string2state.value(group.state, ConsumerGroupInfo::State::Unknown); + info.protocol = QString::fromStdString(group.protocol); + + QSet topics; + int partitions = 0; + for (auto &member : group.members) { + ConsumerGroupInfo::Member m; + m.host = QString::fromStdString(member.clientHost); + m.id = QString::fromStdString(member.memberId); + m.clientID = QString::fromStdString(member.clientId); + + core::MemberAssignmentInformation memberInfo(member.assignment); + + for (auto &tp : memberInfo.topicPartitions()) { + auto topic = QString::fromStdString(tp.first); + topics.insert(topic); + m.topicPartitions.insert(std::make_pair(topic, tp.second)); + ++partitions; + } + + info.members.emplace_back(m); + } + info.topics = topics.size(); + info.partitions = partitions; + out.emplace_back(info); + } + return out; +} + +} // namespace + +ConsumerModel::ConsumerModel(QObject *parent) + : QAbstractTableModel(parent) + , m_inActive(0) + , m_inEmpty(0) + , m_inRebalancing(0) + , m_inDead(0) +{ + m_headers << "Consumer Group" + << "State" + << "Members" + << "Topics"; +} + +void ConsumerModel::setConfig(const ClusterConfig &broker) +{ + m_config = broker; + loadGroups(); +} + +void ConsumerModel::loadGroups() +{ + std::thread loadThread([this, config = m_config]() { + core::KafkaAdmin admin(config); + + auto [groups, err] = admin.listGroups(); + if (err.isError) { + return; + } + + QVector consumers = convertGroup(groups); + QMetaObject::invokeMethod(this, + "setGroups", + Qt::QueuedConnection, + Q_ARG(QVector, consumers)); + }); + loadThread.detach(); +} + +void ConsumerModel::setGroups(QVector groups) +{ + int inActive = 0; + int inEmpty = 0; + int inRebalancing = 0; + int inDead = 0; + + beginResetModel(); + m_groups.swap(groups); + for (auto &group : m_groups) { + switch (group.state) { + case ConsumerGroupInfo::State::Stable: { + ++inActive; + }; break; + case ConsumerGroupInfo::State::Empty: { + ++inEmpty; + }; break; + case ConsumerGroupInfo::State::Rebalance: { + ++inRebalancing; + }; break; + case ConsumerGroupInfo::State::Dead: { + ++inDead; + }; break; + default: { + // skip unknown + }; break; + } + } + endResetModel(); + setInActive(inActive); + setInEmpty(inEmpty); + setInRebalancing(inRebalancing); + setInDead(inDead); +} + +int ConsumerModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + + return m_groups.size(); +} + +int ConsumerModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + return m_headers.size(); +} + +QVariant ConsumerModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= m_groups.size()) { + return {}; + } + + if (role < Qt::DisplayRole) { + return {}; + } + + if (role == Qt::DisplayRole) { + role = Group + index.column(); + } + + const auto row = index.row(); + auto &group = m_groups[row]; + switch (role) { + case Group: + return group.group; + + case State: + return groupStateToString(group.state); + + case Members: + return group.members.size(); + + case PartitionTopics: + return QString("%1 / %2").arg(group.partitions).arg(group.topics); + + case GroupItem: + return QVariant::fromValue(m_groups[row]); + + default: + return QString{}; + } +} + +QHash ConsumerModel::roleNames() const +{ + static QHash roles{{Qt::DisplayRole, "display"}, + {Group, "group"}, + {State, "state"}, + {Members, "members"}, + {PartitionTopics, "partitionTopics"}, + {GroupItem, "groupItem"}}; + + return roles; +} + +int ConsumerModel::inActive() const +{ + return m_inActive; +} + +void ConsumerModel::setInActive(int val) +{ + m_inActive = val; + emit inActiveChanged(); +} + +int ConsumerModel::inEmpty() const +{ + return m_inEmpty; +} + +void ConsumerModel::setInEmpty(int val) +{ + m_inEmpty = val; + emit inEmptyChanged(); +} + +int ConsumerModel::inRebalancing() const +{ + return m_inRebalancing; +} + +void ConsumerModel::setInRebalancing(int val) +{ + m_inRebalancing = val; + emit inRebalancingChanged(); +} + +int ConsumerModel::inDead() const +{ + return m_inDead; +} + +void ConsumerModel::setInDead(int val) +{ + m_inDead = val; + emit inDeadChanged(); +} + +ConsumerFilterModel::ConsumerFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + setFilterRole(ConsumerModel::Group); + setFilterCaseSensitivity(Qt::CaseInsensitive); +} + +void ConsumerFilterModel::setModel(ConsumerModel *model) +{ + setSourceModel(model); +} +ConsumerModel *ConsumerFilterModel::model() const +{ + return dynamic_cast(sourceModel()); +} + +QString ConsumerFilterModel::filter() const +{ + return m_filter; +} + +void ConsumerFilterModel::setFilter(const QString &topic) +{ + m_filter = topic; + setFilterFixedString(m_filter); +} + +ConsumerGroupInfoItem::ConsumerGroupInfoItem(QObject *parent) + : QObject(parent) + , m_members(new GroupMemberModel(this)) +{} + +void ConsumerGroupInfoItem::setGroupInfo(const QVariant &info) +{ + if (info.isNull() || !info.isValid()) { + return; + } + + m_group = info.value(); + m_members->setMembers(m_group.members); + sendChangeSignals(); +} + +void ConsumerGroupInfoItem::sendChangeSignals() +{ + emit nameChanged(); + emit stateChanged(); + emit topicsChanged(); + emit partitionsChanged(); + emit strategyChanged(); +} + +QString ConsumerGroupInfoItem::name() const +{ + return m_group.group; +} + +QString ConsumerGroupInfoItem::strategy() const +{ + return m_group.protocol; +} + +QString ConsumerGroupInfoItem::state() const +{ + switch (m_group.state) { + case ConsumerGroupInfo::State::Stable: + return QLatin1String("Stable"); + case ConsumerGroupInfo::State::Empty: + return QLatin1String("Empty"); + case ConsumerGroupInfo::State::Rebalance: + return QLatin1String("Rebalance"); + case ConsumerGroupInfo::State::Dead: + return QLatin1String("Dead"); + default: + return QLatin1String("Unknown state"); + } +} + +int ConsumerGroupInfoItem::topics() const +{ + return m_group.topics; +} + +int ConsumerGroupInfoItem::partitions() const +{ + return m_group.partitions; +} + +GroupMemberModel *ConsumerGroupInfoItem::members() +{ + return m_members; +} + +GroupMemberModel::GroupMemberModel(QObject *parent) + : QAbstractListModel(parent) +{} + +int GroupMemberModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_members.size(); +} + +QVariant GroupMemberModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.row() >= m_members.size()) { + return {}; + } + + if (role < Qt::DisplayRole) { + return {}; + } + + const auto row = index.row(); + auto &member = m_members[row]; + + switch (role) { + case MemberID: + return member.id; + case ClientID: + return member.clientID; + case HostName: + return member.host; + case Partitions: + return member.topicPartitions.size(); + case TopicsPartitions: + return topicPartitions(member.topicPartitions); + + default: + return QString{}; + } +} + +void GroupMemberModel::setMembers(const QVector &members) +{ + beginResetModel(); + m_members = members; + endResetModel(); +} + +QHash GroupMemberModel::roleNames() const +{ + static QHash roles{ + {MemberID, "memberID"}, + {ClientID, "clientID"}, + {HostName, "host"}, + {Partitions, "partitions"}, + {TopicsPartitions, "topicsPartitions"}, + }; + return roles; +} + +QStringList GroupMemberModel::topicPartitions( + const QSet &tps) const +{ + QStringList list; + list.reserve(tps.size()); + + for (const auto &tp : tps) { + list << QString("%1-%2").arg(tp.first).arg(tp.second); + } + return list; +} \ No newline at end of file diff --git a/src/components/ConsumerModel.h b/src/components/ConsumerModel.h new file mode 100644 index 0000000..a477e2c --- /dev/null +++ b/src/components/ConsumerModel.h @@ -0,0 +1,170 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "ClusterConfig.h" + +struct ConsumerGroupInfo +{ + Q_GADGET +public: + enum class State { Dead, Empty, Rebalance, Stable, Unknown }; + + QString group; + State state; + QString protocol; + struct Member + { + using Topic = QString; + using Partition = int32_t; + using TopicParition = std::pair; + + QString id; + QString clientID; + QString host; + QSet topicPartitions; + }; + QVector members; + int topics; + int partitions; +}; +Q_DECLARE_METATYPE(ConsumerGroupInfo) + +/**! + * Show consumer groups + */ +class ConsumerModel : public QAbstractTableModel +{ + Q_OBJECT + Q_PROPERTY(int inActive READ inActive NOTIFY inActiveChanged) + Q_PROPERTY(int inEmpty READ inEmpty NOTIFY inEmptyChanged) + Q_PROPERTY(int inRebalancing READ inRebalancing NOTIFY inRebalancingChanged) + Q_PROPERTY(int inDead READ inDead NOTIFY inDeadChanged) + +public: + enum Roles { Group = Qt::UserRole + 1, State, Members, PartitionTopics, GroupItem }; + + explicit ConsumerModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void setConfig(const ClusterConfig &broker); + + int inActive() const; + int inEmpty() const; + int inRebalancing() const; + int inDead() const; + +signals: + void inActiveChanged(); + void inEmptyChanged(); + void inRebalancingChanged(); + void inDeadChanged(); + +private slots: + + void setGroups(QVector groups); + +private: + void loadGroups(); + void setInActive(int val); + void setInEmpty(int val); + void setInRebalancing(int val); + void setInDead(int val); + +private: + QVector m_headers; + ClusterConfig m_config; + QVector m_groups; + int m_inActive; + int m_inEmpty; + int m_inRebalancing; + int m_inDead; +}; + +class ConsumerFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(ConsumerModel *model READ model WRITE setModel) + Q_PROPERTY(QString filter READ filter WRITE setFilter) + +public: + ConsumerFilterModel(QObject *parent = nullptr); + + void setModel(ConsumerModel *model); + ConsumerModel *model() const; + + QString filter() const; + void setFilter(const QString &topic); + +private: + QString m_filter; +}; + +class GroupMemberModel; + +class ConsumerGroupInfoItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariant group WRITE setGroupInfo) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString state READ state NOTIFY stateChanged) + Q_PROPERTY(QString strategy READ strategy NOTIFY strategyChanged) + Q_PROPERTY(int topics READ topics NOTIFY topicsChanged) + Q_PROPERTY(int partitions READ partitions NOTIFY partitionsChanged) + Q_PROPERTY(GroupMemberModel *members READ members NOTIFY membersChanged) + +public: + explicit ConsumerGroupInfoItem(QObject *parent = nullptr); + + void setGroupInfo(const QVariant &info); + QString name() const; + QString state() const; + QString strategy() const; + int topics() const; + int partitions() const; + GroupMemberModel *members(); + +signals: + + void nameChanged(); + void stateChanged(); + void topicsChanged(); + void partitionsChanged(); + void strategyChanged(); + void membersChanged(); + +private: + void sendChangeSignals(); + +private: + ConsumerGroupInfo m_group; + GroupMemberModel *m_members; +}; + +class GroupMemberModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles { MemberID = Qt::UserRole + 1, ClientID, HostName, Partitions, TopicsPartitions }; + + explicit GroupMemberModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + void setMembers(const QVector &members); + +private: + QStringList topicPartitions(const QSet &tps) const; + +private: + QVector m_members; +}; \ No newline at end of file diff --git a/src/components/Helpers.cpp b/src/components/Helpers.cpp index f891e86..d173473 100644 --- a/src/components/Helpers.cpp +++ b/src/components/Helpers.cpp @@ -7,6 +7,7 @@ #include "ConfigModel.h" #include "Consumer.h" #include "ConsumerHelperModels.h" +#include "ConsumerModel.h" #include "ExportImportFabric.h" #include "HexView.h" #include "KafkaConnectivityTester.h" @@ -47,6 +48,7 @@ void registerTypes() qmlRegisterAnonymousType("plumber", 1); qmlRegisterAnonymousType("plumber", 1); qmlRegisterAnonymousType("plumber", 1); + qmlRegisterAnonymousType("plumber", 1); qmlRegisterType("plumber", 1, 0, "HexView"); qmlRegisterType("plumber", 1, 0, "FileDataSource"); @@ -55,4 +57,7 @@ void registerTypes() qmlRegisterAnonymousType("HexView", 1); qmlRegisterType("plumber", 1, 0, "ExportImportFabric"); + qmlRegisterType("plumber", 1, 0, "ConsumerFilterModel"); + qmlRegisterType("plumber", 1, 0, "ConsumerGroupInfoItem"); + qmlRegisterAnonymousType("plumber", 1); } \ No newline at end of file diff --git a/src/components/MessageModel.cpp b/src/components/MessageModel.cpp index eb2009b..4cc2f85 100644 --- a/src/components/MessageModel.cpp +++ b/src/components/MessageModel.cpp @@ -26,13 +26,13 @@ MessageModel::MessageModel(QObject *parent) int MessageModel::rowCount(const QModelIndex &index) const { Q_UNUSED(index) - return int(m_records.size()); + return static_cast(m_records.size()); } int MessageModel::columnCount(const QModelIndex &index) const { Q_UNUSED(index) - return int(m_headers.size()); + return static_cast(m_headers.size()); } QVariant MessageModel::data(const QModelIndex &index, int role) const @@ -96,7 +96,7 @@ QVariant MessageModel::data(const QModelIndex &index, int role) const return QString{}; default: - return {}; + return QString{}; } } @@ -131,7 +131,7 @@ void MessageModel::append(core::ConsumerRecords &&records) return; } - beginInsertRows(QModelIndex(), 0, int(records.size() - 1)); + beginInsertRows(QModelIndex(), 0, static_cast(records.size() - 1)); for (const auto &record : records) { m_records.push_front(record); } @@ -139,7 +139,7 @@ void MessageModel::append(core::ConsumerRecords &&records) if (m_records.size() > MaxMessages) { const auto delta = m_records.size() - MaxMessages; - beginRemoveRows(QModelIndex(), MaxMessages, int(MaxMessages + delta)); + beginRemoveRows(QModelIndex(), MaxMessages, static_cast(MaxMessages + delta)); while (m_records.size() > MaxMessages) { delete m_records.back(); m_records.removeLast(); @@ -172,14 +172,14 @@ Message MessageModel::getMessage(int index) const void MessageModel::clear() { - beginRemoveRows(QModelIndex{}, 0, int(m_records.size())); + beginRemoveRows(QModelIndex{}, 0, static_cast(m_records.size())); m_records.clear(); endRemoveRows(); } void MessageModel::exportMessages(std::unique_ptr exporter) { - for (auto record : m_records) { + for (auto *record : m_records) { exporter->writeRecord(record); } } \ No newline at end of file diff --git a/src/core/AdminClient.hpp b/src/core/AdminClient.hpp index 6a67418..f804e76 100644 --- a/src/core/AdminClient.hpp +++ b/src/core/AdminClient.hpp @@ -2,8 +2,11 @@ #include +#include "AdminCommon.hpp" +#include "KafkaHelper.hpp" namespace core { + class AdminClient : public kafka::clients::AdminClient { public: @@ -12,6 +15,8 @@ class AdminClient : public kafka::clients::AdminClient {} Optional fetchNodesMetadata(std::chrono::milliseconds timeout); + + ListGroupsResult listGroups(const std::string &group, std::chrono::milliseconds timeout); }; inline Optional AdminClient::fetchNodesMetadata( @@ -43,4 +48,66 @@ inline Optional AdminClient::fetchNodesMetadata( ret = metadata; return ret; } + +inline ListGroupsResult AdminClient::listGroups(const std::string &group, + std::chrono::milliseconds timeout) +{ + const rd_kafka_group_list *rk_group_list = nullptr; + rd_kafka_resp_err_t err = rd_kafka_list_groups(getClientHandle(), + group.empty() ? nullptr : group.c_str(), + &rk_group_list, + convertMsDurationToInt(timeout)); + auto guard = rd_kafka_group_list_unique_ptr(rk_group_list); + + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) { + return ListGroupsResult(kafka::Error{err, rd_kafka_err2str(err)}); + } + + std::vector groups; + if (rk_group_list->group_cnt == 0) { + return ListGroupsResult(groups); + } + + groups.reserve(rk_group_list->group_cnt); + + for (int i = 0; i < rk_group_list->group_cnt; ++i) { + auto groupItem = rk_group_list->groups[i]; + + GroupInfo::Broker broker(groupItem.broker.id, groupItem.broker.host, groupItem.broker.port); + + GroupInfo info(broker); + + info.group = groupItem.group; + info.error = kafka::Error{groupItem.err, rd_kafka_err2str(err)}; + info.state = groupItem.state; + info.protocolType = groupItem.protocol_type; + info.protocol = groupItem.protocol; + + std::vector members; + members.reserve(groupItem.member_cnt); + for (int j = 0; j < groupItem.member_cnt; ++j) { + auto memberItem = groupItem.members[j]; + GroupInfo::Member member; + member.memberId = memberItem.member_id; + member.clientId = memberItem.client_id; + member.clientHost = memberItem.client_host; + + auto *metaPtr = static_cast(memberItem.member_metadata); + member.metadata.insert(member.metadata.end(), + metaPtr, + metaPtr + memberItem.member_assignment_size); + + auto *assignmentPtr = static_cast(memberItem.member_assignment); + member.assignment.insert(member.assignment.end(), + assignmentPtr, + assignmentPtr + memberItem.member_assignment_size); + members.emplace_back(member); + } + + info.members.swap(members); + groups.emplace_back(info); + } + + return ListGroupsResult(groups); } +} // namespace core diff --git a/src/core/AdminCommon.hpp b/src/core/AdminCommon.hpp new file mode 100644 index 0000000..9e73bc7 --- /dev/null +++ b/src/core/AdminCommon.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "GroupInfo.h" +#include +#include + +namespace core { +/** + * The result of AdminClient::listGroups(). + */ +struct ListGroupsResult +{ + explicit ListGroupsResult(const kafka::Error& err): error(err) {} + explicit ListGroupsResult(std::vector groups): groups(std::move(groups)) {} + + /** + * The result error + */ + kafka::Error error; + + /** + * List for groups + */ + std::vector groups; +}; + +} \ No newline at end of file diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index bb21c13..d27413d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -11,7 +11,9 @@ add_library(core AdminClient.hpp KafkaAdmin.cpp KafkaAdmin.h KafkaStatistic.cpp KafkaStatistic.h Error.cpp Error.h - ConsumerRecordsExporter.cpp ConsumerRecordsExporter.h) + ConsumerRecordsExporter.cpp ConsumerRecordsExporter.h + GroupInfo.cpp GroupInfo.h +) target_link_libraries(core PRIVATE diff --git a/src/core/GroupInfo.cpp b/src/core/GroupInfo.cpp new file mode 100644 index 0000000..233aa6a --- /dev/null +++ b/src/core/GroupInfo.cpp @@ -0,0 +1,77 @@ +#include + +#include "GroupInfo.h" +#include "spdlog/spdlog.h" + +namespace core { +GroupInfo::GroupInfo(Broker broker) + : broker(std::move(broker)){ + + }; + +MemberAssignmentInformation::MemberAssignmentInformation(const std::vector &data) +{ + // Version + topic list size + if (data.size() < sizeof(uint16_t) + sizeof(uint32_t)) { + spdlog::error("parse member assignment information: message is malformed"); + return; + } + const uint8_t *ptr = data.data(); + const uint8_t *end = ptr + data.size(); + memcpy(&m_version, ptr, sizeof(m_version)); + m_version = qFromBigEndian(m_version); + ptr += sizeof(m_version); + + uint32_t total_topics; + memcpy(&total_topics, ptr, sizeof(total_topics)); + total_topics = qFromBigEndian(total_topics); + ptr += sizeof(total_topics); + + for (uint32_t i = 0; i != total_topics; ++i) { + if (ptr + sizeof(uint16_t) > end) { + spdlog::error("parse member assignment information: message is malformed"); + return; + } + uint16_t topic_length; + memcpy(&topic_length, ptr, sizeof(topic_length)); + topic_length = qFromBigEndian(topic_length); + ptr += sizeof(topic_length); + + // Check for string length + size of partitions list + if (topic_length > std::distance(ptr, end) + sizeof(uint32_t)) { + spdlog::error("parse member assignment information: message is malformed"); + return; + } + std::string topic_name(ptr, ptr + topic_length); + ptr += topic_length; + + uint32_t total_partitions; + memcpy(&total_partitions, ptr, sizeof(total_partitions)); + total_partitions = qFromBigEndian(total_partitions); + ptr += sizeof(total_partitions); + + if (ptr + total_partitions * sizeof(uint32_t) > end) { + spdlog::error("parse member assignment information: message is malformed"); + return; + } + for (uint32_t j = 0; j < total_partitions; ++j) { + uint32_t partition; + memcpy(&partition, ptr, sizeof(partition)); + partition = qFromBigEndian(partition); + ptr += sizeof(partition); + + m_topic_partitions.insert(std::make_pair(topic_name, partition)); + } + } +} + +uint16_t MemberAssignmentInformation::version() const +{ + return m_version; +} + +const kafka::TopicPartitions &MemberAssignmentInformation::topicPartitions() const +{ + return m_topic_partitions; +} +} // namespace core \ No newline at end of file diff --git a/src/core/GroupInfo.h b/src/core/GroupInfo.h new file mode 100644 index 0000000..e68574b --- /dev/null +++ b/src/core/GroupInfo.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include + +namespace core { +/** + * The group info + */ +struct GroupInfo +{ + struct Broker + { + public: + using Id = int; + using Host = std::string; + using Port = int; + + Broker(Id i, Host h, Port p) + : id(i) + , host(std::move(h)) + , port(p) + {} + + /** + * The node id. + */ + Broker::Id id; + + /** + * The host name. + */ + Broker::Host host; + + /** + * The port. + */ + Broker::Port port; + + /** + * Obtains explanatory string. + */ + std::string toString() const + { + return host + ":" + std::to_string(port) + "/" + std::to_string(id); + } + }; + + /** + * Originating broker info + */ + Broker broker; + + /** + * The group name + */ + std::string group; + + /** + * Broker-originated error + */ + kafka::Error error; + + std::string state; + + std::string protocolType; + std::string protocol; + + struct Member + { + using Host = std::string; + + std::string memberId; + std::string clientId; + Member::Host clientHost; + std::vector metadata; + std::vector assignment; + }; + + std::vector members; + + explicit GroupInfo(Broker broker); +}; + +class MemberAssignmentInformation +{ +public: + /** + * Constructs an instance + */ + MemberAssignmentInformation(const std::vector &data); + + /** + * Gets the version + */ + uint16_t version() const; + + /** + * Gets the topic/partition assignment + */ + const kafka::TopicPartitions &topicPartitions() const; + +private: + uint16_t m_version; + kafka::TopicPartitions m_topic_partitions; +}; +} // namespace core \ No newline at end of file diff --git a/src/core/KafkaAdmin.cpp b/src/core/KafkaAdmin.cpp index 537b415..947b517 100644 --- a/src/core/KafkaAdmin.cpp +++ b/src/core/KafkaAdmin.cpp @@ -23,7 +23,7 @@ std::tuple KafkaAdmin::listTopics(std::chrono::millis const auto response = client.listTopics(timeout); if (!response.error) { Topics topics; - topics.reserve(qsizetype(response.topics.size())); + topics.reserve(static_cast(response.topics.size())); for (const auto &topic : response.topics) { topics.emplaceBack(QString::fromStdString(topic)); } @@ -31,7 +31,6 @@ std::tuple KafkaAdmin::listTopics(std::chrono::millis } spdlog::error("list topic error: {}", response.error.message()); - return std::make_tuple(Topics{}, Error{when, QString::fromStdString(response.error.message())}); @@ -58,7 +57,7 @@ std::optional KafkaAdmin::deleteTopics(const Topics &topics, toDelete.insert(topic.toStdString()); } - auto result = client.deleteTopics(toDelete, timeout); + const auto result = client.deleteTopics(toDelete, timeout); if (result.error) { spdlog::error("topic remove error {}", result.error.message()); return Error{when, QString::fromStdString(result.error.message())}; @@ -84,7 +83,7 @@ std::tuple, Error> KafkaAdmin::fetchNo core::AdminClient client(cfg); - auto md = client.fetchNodesMetadata(timeout); + const auto md = client.fetchNodesMetadata(timeout); return std::make_tuple(md, Error{}); } catch (const kafka::KafkaException &e) { @@ -112,7 +111,11 @@ std::optional KafkaAdmin::createTopics(const Topics &topics, } core::AdminClient client(cfg); - auto res = client.createTopics(t, numPartitions, replicationFactor, topicConfig, timeout); + const auto res = client.createTopics(t, + numPartitions, + replicationFactor, + topicConfig, + timeout); if (res.error) { return Error{when, QString::fromStdString(res.error.message())}; } @@ -123,4 +126,30 @@ std::optional KafkaAdmin::createTopics(const Topics &topics, return {}; } +std::tuple, Error> KafkaAdmin::listGroups(const QString &group, + std::chrono::milliseconds timeout) +{ + using namespace kafka::clients::admin; + const QString when("list group"); + + try { + Config cfg(m_cfg.properties->map()); + cfg.put(Config::BOOTSTRAP_SERVERS, m_cfg.bootstrap.toStdString()); + AdminClient client(cfg); + + const auto groupFilter = group.toStdString(); + const auto result = client.listGroups(groupFilter, timeout); + if (result.error) { + spdlog::error("list group error {}", result.error.toString()); + return std::make_tuple(std::vector(), + Error(when, QString::fromStdString(result.error.message()))); + } + + return std::make_tuple(result.groups, Error()); + } catch (const kafka::KafkaException &e) { + spdlog::error("unexpected exception caught: {}", e.what()); + return std::make_tuple(std::vector(), Error(when, e.what())); + } +} + } // namespace core \ No newline at end of file diff --git a/src/core/KafkaAdmin.h b/src/core/KafkaAdmin.h index 78c6dd7..5c637ba 100644 --- a/src/core/KafkaAdmin.h +++ b/src/core/KafkaAdmin.h @@ -8,6 +8,7 @@ #include "ClusterConfig.h" #include "Error.h" +#include "GroupInfo.h" namespace core { @@ -67,6 +68,16 @@ class KafkaAdmin : public QObject const kafka::Properties &topicConfig, std::chrono::milliseconds timeout = std::chrono::milliseconds(DefaultCommandTimeoutMS)); + /*! + * list groups + * @param group select one group + * @param timeout + * @return groups and error + */ + std::tuple, Error> listGroups( + const QString &group = QString(), + std::chrono::milliseconds timeout = std::chrono::milliseconds(DefaultCommandTimeoutMS)); + private: ClusterConfig m_cfg; }; diff --git a/src/core/KafkaHelper.hpp b/src/core/KafkaHelper.hpp new file mode 100644 index 0000000..234ea9b --- /dev/null +++ b/src/core/KafkaHelper.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace core { +struct RtListGroupDeleter +{ + void operator()(const rd_kafka_group_list *p) { rd_kafka_group_list_destroy(p); } +}; +using rd_kafka_group_list_unique_ptr = std::unique_ptr; +} // namespace core \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 0b940b4..c4d18a8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -96,7 +96,7 @@ int main(int argc, char *argv[]) Qt::QueuedConnection); engine.rootContext()->setContextProperty("errorService", &Services->errors()); - engine.setObjectOwnership(&Services->errors(), QJSEngine::CppOwnership); + QJSEngine::setObjectOwnership(&Services->errors(), QJSEngine::CppOwnership); engine.load(url); diff --git a/src/qml.qrc b/src/qml.qrc index 5f810fb..c8a6aef 100644 --- a/src/qml.qrc +++ b/src/qml.qrc @@ -72,5 +72,8 @@ qml/Components/BlueButton.qml qml/Consumer/AvroOptions.qml qml/Producer/AvroOptions.qml + qml/Group/ConsumerTableView.qml + qml/Group/GroupsView.qml + qml/Group/GroupView.qml diff --git a/src/qml/Brokers.qml b/src/qml/Brokers.qml index e663706..0fb01de 100644 --- a/src/qml/Brokers.qml +++ b/src/qml/Brokers.qml @@ -1,6 +1,6 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import "style.js" as Style import "Components" as Components diff --git a/src/qml/Consumer/MessageTableView.qml b/src/qml/Consumer/MessageTableView.qml index 34a95de..cf0d8f2 100644 --- a/src/qml/Consumer/MessageTableView.qml +++ b/src/qml/Consumer/MessageTableView.qml @@ -205,18 +205,18 @@ Rectangle { font.bold: true } - Rectangle { + Item { id: splitter - color: Style.BorderColor height: parent.height - width: 1 + width: 5 visible: headerHover.containsMouse - x: columnWidths[index] - 1 + x: columnWidths[index] - 5 + onXChanged: { if (drag.active) { main.columnWidths[index] = splitter.x; - root.width = splitter.x + 1; + root.width = splitter.x + 5; view.forceLayout(); } } @@ -228,6 +228,13 @@ Rectangle { xAxis.enabled: true cursorShape: Qt.SizeHorCursor } + + Rectangle { + anchors.centerIn: parent + color: Style.BorderColor + height: parent.height + width: 1 + } } } } diff --git a/src/qml/Consumers.qml b/src/qml/Consumers.qml index b79b519..faa6633 100644 --- a/src/qml/Consumers.qml +++ b/src/qml/Consumers.qml @@ -1,12 +1,58 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import plumber import "style.js" as Style +import "Components" as Components +import "Group" as Group -Rectangle { +Item { id: item width: 300 height: 150 - border.color: Style.BorderColor + + StackView { + id: groupStack + anchors.fill: parent + initialItem: groupsView + + pushEnter: Transition { + } + + pushExit: Transition { + } + + popEnter: Transition { + } + + popExit: Transition { + } + } + + Component { + id: groupsView + Group.GroupsView { + width: item.width + height: item.height + + onSelectedGroup: group => { + let obj = groupView.createObject(item, { + "group": group + }); + groupStack.push(obj); + } + + consumerModel: mainCluster.consumerModel() + } + } + + Component { + id: groupView + + Group.GroupView { + width: item.width + height: item.height + } + } } diff --git a/src/qml/Group/ConsumerTableView.qml b/src/qml/Group/ConsumerTableView.qml new file mode 100644 index 0000000..1e467ec --- /dev/null +++ b/src/qml/Group/ConsumerTableView.qml @@ -0,0 +1,188 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import "../style.js" as Style +import "../pages.js" as Pages +import "../Components" as Components + +Item { + id: main + signal selectedGroup(var group) + + clip: true + property var model + property int rowHeight: 40 + property var columnWidths: [905, 115, 95, 90] + function columnWidthProvider(column) { + return columnWidths[column]; + } + + ListModel { + id: headerModel + + ListElement { + name: "Consumer Group" + } + + ListElement { + name: "State" + } + + ListElement { + name: "Members" + } + + ListElement { + name: "Topics" + } + } + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + + MouseArea { + id: headerHover + + width: parent.width + height: parent.height + hoverEnabled: true + } + + Row { + id: row + + Repeater { + model: headerModel + + Item { + id: root + + width: columnWidths[index] + height: 30 + + Text { + anchors.centerIn: parent + text: model.name + font.bold: true + } + + Item { + id: splitter + + height: parent.height + width: 5 + visible: headerHover.containsMouse + x: columnWidths[index] - 5 + + onXChanged: { + if (drag.active) { + main.columnWidths[index] = splitter.x; + root.width = splitter.x + 5; + view.forceLayout(); + } + } + + DragHandler { + id: drag + + yAxis.enabled: false + xAxis.enabled: true + cursorShape: Qt.SizeHorCursor + } + + Rectangle { + anchors.centerIn: parent + color: Style.BorderColor + height: parent.height + width: 1 + } + } + } + } + } + } + + TableView { + id: view + Layout.fillHeight: true + Layout.fillWidth: true + + columnWidthProvider: main.columnWidthProvider + clip: true + model: main.model + boundsMovement: Flickable.StopAtBounds + + delegate: Item { + implicitWidth: 100 + implicitHeight: rowHeight + + StackLayout { + anchors.fill: parent + currentIndex: column + + Components.TextButton { + // topic group + text: display + leftPadding: 8 + Layout.fillHeight: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + color: "#2a5fb0" + onClickecd: { + main.selectedGroup(groupItem); + } + } + + Text { + // state + text: display + color: Style.LabelColor + Layout.fillHeight: true + Layout.fillWidth: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + Text { + // members + text: display + color: Style.LabelColor + Layout.fillHeight: true + Layout.fillWidth: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + Text { + // partition/topics + text: display + color: Style.LabelColor + Layout.fillHeight: true + Layout.fillWidth: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + } + + ScrollBar.vertical: ScrollBar { + id: tableVerticalBar + + policy: ScrollBar.AsNeeded + minimumSize: 0.06 + } + + ScrollBar.horizontal: ScrollBar { + policy: ScrollBar.AsNeeded + minimumSize: 0.06 + } + } + } +} diff --git a/src/qml/Group/GroupView.qml b/src/qml/Group/GroupView.qml new file mode 100644 index 0000000..672c181 --- /dev/null +++ b/src/qml/Group/GroupView.qml @@ -0,0 +1,198 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import plumber +import "../style.js" as Style + +Item { + width: 100 + height: 100 + property alias group: gInfo.group + + ConsumerGroupInfoItem { + id: gInfo + } + + ColumnLayout { + anchors.fill: parent + spacing: 16 + + Button { + icon.source: "qrc:/left.svg" + implicitWidth: 28 + implicitHeight: 28 + onClicked: { + groupStack.pop(); + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 200 + + border.width: 1 + border.color: Style.BorderColor + + ColumnLayout { + anchors.fill: parent + + Text { + + text: qsTr("CONSUMER GROUP") + " " + gInfo.name + font.pixelSize: 24 + color: Style.LabelColorDark + Layout.margins: 8 + } + + GridLayout { + Layout.leftMargin: 8 + columns: 2 + + Text { + text: qsTr("State:") + font.pixelSize: 18 + font.bold: true + color: Style.LabelColor + } + Text { + text: gInfo.state + font.pixelSize: 18 + color: Style.LabelColor + } + + Text { + text: qsTr("Assigned Topics:") + font.pixelSize: 18 + font.bold: true + color: Style.LabelColor + } + Text { + text: gInfo.topics + font.pixelSize: 18 + color: Style.LabelColor + } + + Text { + text: qsTr("Assigned Partitions:") + font.pixelSize: 18 + font.bold: true + color: Style.LabelColor + } + Text { + text: gInfo.partitions + font.pixelSize: 18 + color: Style.LabelColor + } + + Text { + text: qsTr("Strategy:") + font.pixelSize: 18 + font.bold: true + color: Style.LabelColor + } + Text { + text: gInfo.strategy + font.pixelSize: 18 + color: Style.LabelColor + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + + Rectangle { + width: 100 + height: 100 + + Layout.fillWidth: true + Layout.fillHeight: true + + border.width: 1 + border.color: Style.BorderColor + + ListView { + clip: true + anchors.fill: parent + model: gInfo.members + + delegate: Item { + height: 100 + width: parent.width + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + + GridLayout { + clip: true + columns: 2 + + Text { + text: qsTr("Member ID:") + font.bold: true + color: Style.LabelColor + } + Text { + text: memberID + color: Style.LabelColor + } + + Text { + text: qsTr("Client ID:") + font.bold: true + color: Style.LabelColor + } + Text { + text: clientID + color: Style.LabelColor + } + + Text { + text: qsTr("Hostname:") + font.bold: true + color: Style.LabelColor + } + Text { + text: host + color: Style.LabelColor + } + + Text { + text: qsTr("Total Partitions:") + font.bold: true + color: Style.LabelColor + } + Text { + text: partitions + color: Style.LabelColor + } + } + + ListView { + Layout.leftMargin: 36 + Layout.fillWidth: true + Layout.fillHeight: true + + model: topicsPartitions + spacing: 4 + delegate: Text { + text: modelData + } + } + } + + Rectangle { + height: 1 + width: parent.width + anchors.bottom: parent.bottom + color: "#f2f2f2" + } + } + } + } + } +} diff --git a/src/qml/Group/GroupsView.qml b/src/qml/Group/GroupsView.qml new file mode 100644 index 0000000..c5485d1 --- /dev/null +++ b/src/qml/Group/GroupsView.qml @@ -0,0 +1,224 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import plumber +import "../style.js" as Style +import "../Components" as Components +import "../" + +Rectangle { + id: item + property var consumerModel: mainCluster.consumerModel() + signal selectedGroup(var group) + + width: 300 + height: 150 + border.color: Style.BorderColor + + ConsumerFilterModel { + id: groupFilterModel + + model: consumerModel + filter: filterField.text + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + OverviewItem { + Layout.fillWidth: true + text: qsTr("CONSUMERS") + onClicked: overview.activatedItem(Constants.ConsumersIndex) + + content: Item { + RowLayout { + anchors.fill: parent + anchors.margins: 16 + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inActive + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Active") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Rectangle { + width: 1 + Layout.fillHeight: true + color: Style.BorderColor + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inEmpty + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Empty") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Rectangle { + width: 1 + Layout.fillHeight: true + color: Style.BorderColor + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inRebalancing + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Rebalancing") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Rectangle { + width: 1 + Layout.fillHeight: true + color: Style.BorderColor + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inDead + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Dead") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Item { + Layout.fillWidth: true + } + } + } + } + + Item { + implicitWidth: 250 + Layout.fillWidth: true + height: 60 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 6 + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + + Item { + Layout.fillWidth: true + } + + TextField { + id: filterField + Layout.preferredWidth: 180 + persistentSelection: true + selectByMouse: true + placeholderText: qsTr("Filter consumer group name...") + } + } + + Rectangle { + height: 1 + width: parent.width + anchors.bottom: parent.bottom + color: "#f2f2f2" + } + } + + ConsumerTableView { + Layout.topMargin: 2 + Layout.fillWidth: true + Layout.fillHeight: true + + onSelectedGroup: group => { + item.selectedGroup(group); + } + + model: groupFilterModel + } + } +} diff --git a/src/qml/LeftPanel/Menu.qml b/src/qml/LeftPanel/Menu.qml index 7aad7d1..9ace11b 100644 --- a/src/qml/LeftPanel/Menu.qml +++ b/src/qml/LeftPanel/Menu.qml @@ -62,15 +62,16 @@ Item { } } - /* - MenuItem{ + MenuItem { index: 3 text: qsTr("Consumers") - icon: "qrc:/images/consumers.svg" + icon: "qrc:/consumers.svg" Layout.fillWidth: true - onClicked: menu.onClickHandler(index) + onClicked: index => { + menu.onClickHandler(index); + } } -*/ + Item { Layout.fillHeight: true Layout.fillWidth: true diff --git a/src/qml/Overview.qml b/src/qml/Overview.qml index 180b2d8..9267781 100644 --- a/src/qml/Overview.qml +++ b/src/qml/Overview.qml @@ -9,6 +9,7 @@ Item { property var brokerModel: mainCluster.brokerModel() property var topicModel: mainCluster.topicModel() + property var consumerModel: mainCluster.consumerModel() signal activatedItem(int indx) @@ -98,18 +99,157 @@ Item { } } - /* OverviewItem { Layout.fillWidth: true text: qsTr("CONSUMERS") - content: Rectangle{ - width: 10 - height: 10 - color: "green" + onClicked: overview.activatedItem(Constants.ConsumersIndex) + + content: Item { + RowLayout { + anchors.fill: parent + anchors.margins: 16 + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inActive + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Active") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Rectangle { + width: 1 + Layout.fillHeight: true + color: Style.BorderColor + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inEmpty + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Empty") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Rectangle { + width: 1 + Layout.fillHeight: true + color: Style.BorderColor + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inRebalancing + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Rebalancing") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Rectangle { + width: 1 + Layout.fillHeight: true + color: Style.BorderColor + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: 150 + + ColumnLayout { + anchors.fill: parent + spacing: 8 + + Text { + text: consumerModel.inDead + font.bold: true + font.pixelSize: 22 + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Text { + font.pixelSize: 22 + text: qsTr("Dead") + color: Style.LabelColor + Layout.alignment: Qt.AlignCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + Item { + Layout.fillWidth: true + } + } } - onClicked: overview.activatedItem(Constants.ConsumersIndex); } -*/ + Item { Layout.fillHeight: true Layout.fillWidth: true diff --git a/src/qml/OverviewItem.qml b/src/qml/OverviewItem.qml index a5a1594..f747ac2 100644 --- a/src/qml/OverviewItem.qml +++ b/src/qml/OverviewItem.qml @@ -11,7 +11,7 @@ Rectangle { signal clicked width: 300 - height: 130 + height: 140 border.color: Style.BorderColor ColumnLayout { diff --git a/src/qml/Topics.qml b/src/qml/Topics.qml index e98185e..4a7d5f5 100644 --- a/src/qml/Topics.qml +++ b/src/qml/Topics.qml @@ -1,7 +1,7 @@ -import QtQuick 2.12 -import QtQuick.Controls 2.12 -import QtQuick.Layouts 1.12 -import plumber 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import plumber import "style.js" as Style import "pages.js" as Pages import "Components" as Components @@ -178,6 +178,7 @@ Rectangle { TextField { id: filterField + Layout.preferredWidth: 180 persistentSelection: true selectByMouse: true diff --git a/win/build.ps1 b/win/build.ps1 index e2f3fc4..908676f 100644 --- a/win/build.ps1 +++ b/win/build.ps1 @@ -1,9 +1,9 @@ -$Env:Path += ";C:\Qt\6.3.1\msvc2019_64\bin" +$Env:Path += ";C:\Qt\6.3.2\msvc2019_64\bin" $Env:VCINSTALLDIR = "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC" & cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=C:/tools/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release & cmake --build build --config Release -& C:/Qt/6.3.1/msvc2019_64/bin/windeployqt.exe --qmldir src/qml build/Release +& C:/Qt/6.3.2/msvc2019_64/bin/windeployqt.exe --qmldir src/qml build/Release New-Item -ItemType Directory -Force -Path build/Release/Qt/labs/platform Remove-Item build/Release/Qt/labs/platform -Include *.pdb Rename-Item -Path build/Release -NewName plumber