diff --git a/.clang-tidy b/.clang-tidy index bc7273f..c3a70d7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,6 +1,9 @@ Checks: "*,\ -llvm-header-guard,\ -llvm-namespace-comment,\ + -llvmlibc-callee-namespace,\ + -llvmlibc-restrict-system-libc-headers,\ + -llvmlibc-implementation-in-namespace,\ -fuchsia-*,\ -google-default-arguments,\ -google-readability-namespace-comments,\ @@ -31,6 +34,7 @@ Checks: "*,\ -cppcoreguidelines-no-malloc,\ -cppcoreguidelines-non-private-member-variables-in-classes,\ -cppcoreguidelines-pro-bounds-constant-array-index,\ + -cppcoreguidelines-avoid-non-const-global-variables,\ -misc-non-private-member-variables-in-classes,\ -readability-magic-numbers,\ -readability-implicit-bool-conversion,\ @@ -38,5 +42,7 @@ Checks: "*,\ -readability-isolate-declaration,\ -bugprone-macro-parentheses,\ -bugprone-unused-return-value,\ + -bugprone-suspicious-include,\ -cert-err58-cpp,\ - -cert-err60-cpp" + -cert-err60-cpp,\ + -readability-use-anyofallof" diff --git a/CMakeLists.txt b/CMakeLists.txt index a964dc7..95d54a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,27 +14,14 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_THREAD_PREFER_PTHREAD TRUE) set(THREADS_PREFER_PTHREAD_FLAG TRUE) +include(cmake/tools.cmake) if ((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")) add_compile_options("-Wall" "-Werror" "-Wextra" "-Wshadow" "-Wno-unused-result") elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") add_definitions(-D_CRT_SECURE_NO_WARNINGS) endif () -option(BUILD_OPTION_CLANG_TIDY "Build with clang-tidy enabled" OFF) -if (BUILD_OPTION_CLANG_TIDY) - find_program(CLANG_TIDY_EXE NAMES "clang-tidy") - - if (CLANG_TIDY_EXE) - message(STATUS "Use clang-tidy: ${CLANG_TIDY_EXE}") - set(CMAKE_CXX_CLANG_TIDY clang-tidy -warnings-as-errors=* -header-filter='src/.*') - else () - message(FATAL_ERROR "The clang-tidy executable not found!") - endif () - -else () - message(STATUS "With NO clang-tidy build option") -endif () - +clang_tidy(-warnings-as-errors=* -header-filter='${CMAKE_SOURCE_DIR}/*') list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) include_directories(3rdparty/modern-cpp-kafka/include diff --git a/cmake/tools.cmake b/cmake/tools.cmake new file mode 100644 index 0000000..35e1878 --- /dev/null +++ b/cmake/tools.cmake @@ -0,0 +1,115 @@ +# +# Copyright (C) 2018-2020 by George Cave - gcave@stablecoder.ca +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# https://github.com/StableCoder/cmake-scripts + +option(CLANG_TIDY "Turns on clang-tidy processing if it is found." OFF) +option(IWYU "Turns on include-what-you-use processing if it is found." OFF) +option(CPPCHECK "Turns on cppcheck processing if it is found." OFF) + +# Adds clang-tidy checks to the compilation, with the given arguments being used +# as the options set. +macro(clang_tidy) + if(CLANG_TIDY AND CLANG_TIDY_EXE) + set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE} ${ARGN}) + endif() +endmacro() + +# Adds include_what_you_use to the compilation, with the given arguments being +# used as the options set. +macro(include_what_you_use) + if(IWYU AND IWYU_EXE) + set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE ${IWYU_EXE} ${ARGN}) + endif() +endmacro() + +# Adds cppcheck to the compilation, with the given arguments being used as the +# options set. +macro(cppcheck) + if(CPPCHECK AND CPPCHECK_EXE) + set(CMAKE_CXX_CPPCHECK ${CPPCHECK_EXE} ${ARGN}) + endif() +endmacro() + +find_program(CLANG_TIDY_EXE NAMES "clang-tidy") +mark_as_advanced(FORCE CLANG_TIDY_EXE) +if(CLANG_TIDY_EXE) + message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}") + if(NOT CLANG_TIDY) + message(STATUS "clang-tidy NOT ENABLED via 'CLANG_TIDY' variable!") + set(CMAKE_CXX_CLANG_TIDY + "" + CACHE STRING "" FORCE) # delete it + endif() +elseif(CLANG_TIDY) + message(SEND_ERROR "Cannot enable clang-tidy, as executable not found!") + set(CMAKE_CXX_CLANG_TIDY + "" + CACHE STRING "" FORCE) # delete it +else() + message(STATUS "clang-tidy not found!") + set(CMAKE_CXX_CLANG_TIDY + "" + CACHE STRING "" FORCE) # delete it +endif() + +find_program(IWYU_EXE NAMES "include-what-you-use") +mark_as_advanced(FORCE IWYU_EXE) +if(IWYU_EXE) + message(STATUS "include-what-you-use found: ${IWYU_EXE}") + if(NOT IWYU) + message(STATUS "include-what-you-use NOT ENABLED via 'IWYU' variable!") + set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE + "" + CACHE STRING "" FORCE) # delete it + endif() +elseif(IWYU) + message( + SEND_ERROR "Cannot enable include-what-you-use, as executable not found!") + set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE + "" + CACHE STRING "" FORCE) # delete it +else() + message(STATUS "include-what-you-use not found!") + set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE + "" + CACHE STRING "" FORCE) # delete it +endif() + +find_program(CPPCHECK_EXE NAMES "cppcheck") +mark_as_advanced(FORCE CPPCHECK_EXE) +if(CPPCHECK_EXE) + message(STATUS "cppcheck found: ${CPPCHECK_EXE}") + if(CPPECHECK) + set(CMAKE_CXX_CPPCHECK + "${CPPCHECK_EXE};--enable=warning,performance,portability,missingInclude;--template=\"[{severity}][{id}] {message} {callstack} \(On {file}:{line}\)\";--suppress=missingIncludeSystem;--quiet;--verbose;--force" + ) + endif() + if(NOT CPPCHECK) + message(STATUS "cppcheck NOT ENABLED via 'CPPCHECK' variable!") + set(CMAKE_CXX_CPPCHECK + "" + CACHE STRING "" FORCE) # delete it + endif() +elseif(CPPCHECK) + message(SEND_ERROR "Cannot enable cppcheck, as executable not found!") + set(CMAKE_CXX_CPPCHECK + "" + CACHE STRING "" FORCE) # delete it +else() + message(STATUS "cppcheck not found!") + set(CMAKE_CXX_CPPCHECK + "" + CACHE STRING "" FORCE) # delete it +endif() diff --git a/src/components/CMakeLists.txt b/src/components/CMakeLists.txt index ba6169a..81965dd 100644 --- a/src/components/CMakeLists.txt +++ b/src/components/CMakeLists.txt @@ -11,7 +11,10 @@ add_library(components Types.h Types.cpp Message.cpp Message.h TopicCreator.cpp TopicCreator.h - ErrorWrap.cpp ErrorWrap.h Producer.cpp Producer.h) + ErrorWrap.cpp ErrorWrap.h + Producer.cpp Producer.h + ProtoOption.cpp ProtoOption.h + Helpers.cpp Helpers.h) target_compile_definitions(components PRIVATE $<$,$>:QT_QML_DEBUG>) diff --git a/src/components/Consumer.cpp b/src/components/Consumer.cpp index 832ba38..7097f07 100644 --- a/src/components/Consumer.cpp +++ b/src/components/Consumer.cpp @@ -363,23 +363,20 @@ ErrorWrap Consumer::setConverter() return ErrorWrap{}; } - if (m_typeSelector->valueType() == Protobuf) { - if (!m_typeSelector->valueProtoFile().isValid()) { - return ErrorWrap("converter", "path to proto not set"); - } - - if (m_typeSelector->valueProtoMessage().isEmpty()) { - return ErrorWrap("converter", "proto message not select"); + m_consumer->setKeyConverter(nullptr); + m_consumer->setValueConverter(nullptr); + if (m_typeSelector->keyType() == Protobuf) { + auto [converter, err] = m_typeSelector->protoKey()->converter(); + if (converter == nullptr) { + return err; } + m_consumer->setKeyConverter(std::move(converter)); + } - QStringList errors; - auto converter - = formats::protobuf::ProtobufConverter::fabric(m_typeSelector->valueProtoFile(), - m_typeSelector->valueProtoMessage(), - errors); + if (m_typeSelector->valueType() == Protobuf) { + auto [converter, err] = m_typeSelector->protoValue()->converter(); if (converter == nullptr) { - spdlog::error("failed create protobuf converter"); - return ErrorWrap("consumer", errors.join('\n')); + return err; } m_consumer->setValueConverter(std::move(converter)); } @@ -391,6 +388,8 @@ ConsumerTypeSelector::ConsumerTypeSelector(QObject *parent) : QObject(parent) , m_keyType(Types::String) , m_valueType(Types::String) + , m_keyProto(nullptr) + , m_valueProto(nullptr) {} Types ConsumerTypeSelector::keyType() const @@ -415,48 +414,28 @@ void ConsumerTypeSelector::setValueType(Types type) emit typesChanged(); } -const QUrl &ConsumerTypeSelector::keyProtoFile() -{ - return m_keyProtoFile; -} - -void ConsumerTypeSelector::setKeyProtoFile(const QUrl &path) -{ - m_keyProtoFile = path; - emit typesChanged(); -} - -const QString &ConsumerTypeSelector::keyProtoMessage() -{ - return m_keyProtoMessage; -} - -void ConsumerTypeSelector::setKeyProtoMessage(const QString &msg) -{ - m_keyProtoMessage = msg; - emit typesChanged(); -} - -const QUrl &ConsumerTypeSelector::valueProtoFile() +ProtoOption *ConsumerTypeSelector::protoKey() { - return m_valueProtoFile; + return m_keyProto; } -void ConsumerTypeSelector::setValueProtoFile(const QUrl &path) +void ConsumerTypeSelector::setProtoKey(ProtoOption *option) { - m_valueProtoFile = path; + m_keyProto = option; emit typesChanged(); + connect(m_keyProto, &ProtoOption::changed, this, &ConsumerTypeSelector::typesChanged); } -const QString &ConsumerTypeSelector::valueProtoMessage() +ProtoOption *ConsumerTypeSelector::protoValue() { - return m_valueProtoMessage; + return m_valueProto; } -void ConsumerTypeSelector::setValueProtoMessage(const QString &msg) +void ConsumerTypeSelector::setProtoValue(ProtoOption *option) { - m_valueProtoMessage = msg; + m_valueProto = option; emit typesChanged(); + connect(m_valueProto, &ProtoOption::changed, this, &ConsumerTypeSelector::typesChanged); } ConsumerLimiterSelector::ConsumerLimiterSelector(QObject *parent) diff --git a/src/components/Consumer.h b/src/components/Consumer.h index aadd06d..a30025f 100644 --- a/src/components/Consumer.h +++ b/src/components/Consumer.h @@ -9,6 +9,7 @@ #include "ClusterConfig.h" #include "ErrorWrap.h" #include "MessageModel.h" +#include "ProtoOption.h" #include "Types.h" namespace core { @@ -28,11 +29,8 @@ class ConsumerTypeSelector : public QObject Q_PROPERTY(Types keyType READ keyType WRITE setKeyType) Q_PROPERTY(Types valueType READ valueType WRITE setValueType) - Q_PROPERTY(QUrl keyProtoFile READ keyProtoFile WRITE setKeyProtoFile) - Q_PROPERTY(QString keyProtoMessage READ keyProtoMessage WRITE setKeyProtoMessage) - - Q_PROPERTY(QUrl valueProtoFile READ valueProtoFile WRITE setValueProtoFile) - Q_PROPERTY(QString valueProtoMessage READ valueProtoMessage WRITE setValueProtoMessage) + Q_PROPERTY(ProtoOption *protoKey READ protoKey WRITE setProtoKey) + Q_PROPERTY(ProtoOption *protoValue READ protoValue WRITE setProtoValue) explicit ConsumerTypeSelector(QObject *parent = nullptr); @@ -42,17 +40,11 @@ class ConsumerTypeSelector : public QObject Types valueType() const; void setValueType(Types type); - const QUrl &keyProtoFile(); - void setKeyProtoFile(const QUrl &path); - - const QString &keyProtoMessage(); - void setKeyProtoMessage(const QString &msg); + ProtoOption *protoKey(); + void setProtoKey(ProtoOption *option); - const QUrl &valueProtoFile(); - void setValueProtoFile(const QUrl &path); - - const QString &valueProtoMessage(); - void setValueProtoMessage(const QString &msg); + ProtoOption *protoValue(); + void setProtoValue(ProtoOption *option); signals: @@ -62,11 +54,8 @@ class ConsumerTypeSelector : public QObject Types m_keyType; Types m_valueType; - QUrl m_keyProtoFile; - QString m_keyProtoMessage; - - QUrl m_valueProtoFile; - QString m_valueProtoMessage; + ProtoOption *m_keyProto; + ProtoOption *m_valueProto; }; class ConsumerLimiterSelector; diff --git a/src/components/Helpers.cpp b/src/components/Helpers.cpp new file mode 100644 index 0000000..e2a3ab6 --- /dev/null +++ b/src/components/Helpers.cpp @@ -0,0 +1,42 @@ +#include + +#include "Helpers.h" + +#include "Cluster.h" +#include "ConfigModel.h" +#include "Consumer.h" +#include "ConsumerHelperModels.h" +#include "KafkaConnectivityTester.h" +#include "Producer.h" +#include "ProtoOption.h" +#include "TopicCreator.h" + +void registerTypes() +{ + qmlRegisterType("plumber", 1, 0, "ConnectivityTester"); + qmlRegisterType("plumber", 1, 0, "ConfigModel"); + qmlRegisterType("plumber", 1, 0, "Cluster"); + qmlRegisterType("plumber", 1, 0, "TopicFilterModel"); + qmlRegisterType("plumber", 1, 0, "Consumer"); + qmlRegisterType("plumber", 1, 0, "ConsumerTypeSelector"); + qmlRegisterType("plumber", 1, 0, "ConsumerLimiterSelector"); + qmlRegisterType("plumber", 1, 0, "ConsumerFilterSelector"); + qmlRegisterType("plumber", 1, 0, "ConsumerBeginningSelector"); + qmlRegisterType("plumber", 1, 0, "TopicCreator"); + + qmlRegisterType("plumber", 1, 0, "Producer"); + qmlRegisterType("plumber", 1, 0, "ProducerOptions"); + qmlRegisterType("plumber", 1, 0, "ProtoOption"); + + qmlRegisterType("plumber", 1, 0, "ConsumerTypesModel"); + qmlRegisterType("plumber", 1, 0, "CustomTypesModel"); + qmlRegisterType("plumber", 1, 0, "StartFromTimeBasedModel"); + qmlRegisterType("plumber", 1, 0, "FiltersModel"); + qmlRegisterType("plumber", 1, 0, "LimitModel"); + qmlRegisterType("plumber", 1, 0, "MessageWrapper"); + qmlRegisterType("plumber", 1, 0, "HeaderTableModel"); + + qmlRegisterAnonymousType("plumber", 1); + qmlRegisterAnonymousType("plumber", 1); + qmlRegisterAnonymousType("plumber", 1); +} \ No newline at end of file diff --git a/src/components/Helpers.h b/src/components/Helpers.h new file mode 100644 index 0000000..d29e9af --- /dev/null +++ b/src/components/Helpers.h @@ -0,0 +1,3 @@ +#pragma once + +void registerTypes(); \ No newline at end of file diff --git a/src/components/Producer.cpp b/src/components/Producer.cpp index fc1cb88..58c1ea7 100644 --- a/src/components/Producer.cpp +++ b/src/components/Producer.cpp @@ -1,10 +1,13 @@ #include #include +#include "spdlog/spdlog.h" + +#include "AbstractConverter.h" #include "KafkaProducer.h" #include "Producer.h" +#include "ProtoOption.h" #include "Types.h" -#include "spdlog/spdlog.h" Producer::Producer(QObject *parent) : QObject(parent) @@ -13,6 +16,8 @@ Producer::Producer(QObject *parent) , m_keyType(String) , m_valueType(String) , m_logModel(new ProducerLogModel(this)) + , m_keyProto(nullptr) + , m_valueProto(nullptr) {} ProducerOptions *Producer::options() @@ -32,8 +37,33 @@ ErrorWrap Producer::send(const QString &key, const QString &value) createProducer(); } - auto keyData = bytes(m_keyType, key); - auto valueData = bytes(m_valueType, value); + QByteArray keyData; + if (m_keyType == Protobuf) { + if (m_keyConverter == nullptr) { + auto [converter, err] = m_keyProto->converter(); + if (converter == nullptr) { + return err; + } + m_keyConverter.swap(converter); + } + keyData = m_keyConverter->fromJSON(key.toUtf8()); + } else { + keyData = bytes(m_keyType, key); + } + + QByteArray valueData; + if (m_valueType == Protobuf) { + if (m_valueConverter == nullptr) { + auto [converter, err] = m_valueProto->converter(); + if (converter == nullptr) { + return err; + } + m_valueConverter.swap(converter); + } + valueData = m_valueConverter->fromJSON(value.toUtf8()); + } else { + valueData = bytes(m_valueType, value); + } core::ProducerRecord rec{m_topic, keyData, valueData}; if (auto meta = m_producer->send(rec)) { @@ -97,6 +127,47 @@ ProducerLogModel *Producer::log() noexcept return m_logModel; } +ProtoOption *Producer::protoKey() +{ + return m_keyProto; +} + +void Producer::setProtoKey(ProtoOption *option) +{ + if (m_keyProto != nullptr) { + m_keyProto->disconnect(); + resetKeyConverter(); + } + m_keyProto = option; + connect(m_keyProto, &ProtoOption::changed, this, &Producer::resetKeyConverter); +} + +ProtoOption *Producer::protoValue() +{ + return m_valueProto; +} + +void Producer::setProtoValue(ProtoOption *option) +{ + if (m_valueProto != nullptr) { + m_valueProto->disconnect(); + resetValueConverter(); + } + + m_valueProto = option; + connect(m_valueProto, &ProtoOption::changed, this, &Producer::resetValueConverter); +} + +void Producer::resetKeyConverter() +{ + m_keyConverter.release(); +} + +void Producer::resetValueConverter() +{ + m_valueConverter.release(); +} + ProducerOptions::ProducerOptions(QObject *parent) : QObject(parent) , m_compression(Producer::Compression::NoneCompression) diff --git a/src/components/Producer.h b/src/components/Producer.h index 8c5ad88..4b0d934 100644 --- a/src/components/Producer.h +++ b/src/components/Producer.h @@ -6,6 +6,7 @@ #include "ClusterConfig.h" #include "ErrorWrap.h" #include "ProducerRecord.h" +#include "ProtoOption.h" #include "Types.h" namespace core { @@ -27,6 +28,8 @@ class Producer : public QObject Q_PROPERTY(QString topic MEMBER m_topic) Q_PROPERTY(ClusterConfig broker READ broker WRITE setBroker NOTIFY brokerChanged) Q_PROPERTY(ProducerLogModel *log READ log NOTIFY logChanged) + Q_PROPERTY(ProtoOption *protoKey READ protoKey WRITE setProtoKey) + Q_PROPERTY(ProtoOption *protoValue READ protoValue WRITE setProtoValue) public: enum Compression { NoneCompression, GZip, Snappy, LZ4, ZStd }; @@ -46,6 +49,12 @@ class Producer : public QObject ProducerLogModel *log() noexcept; Q_INVOKABLE ErrorWrap send(const QString &key, const QString &value); + ProtoOption *protoKey(); + void setProtoKey(ProtoOption *option); + + ProtoOption *protoValue(); + void setProtoValue(ProtoOption *option); + signals: void brokerChanged(); @@ -54,6 +63,8 @@ class Producer : public QObject private slots: void onOptionsChanged(); + void resetKeyConverter(); + void resetValueConverter(); private: void createProducer(); @@ -66,6 +77,11 @@ private slots: QString m_topic; ClusterConfig m_broker; ProducerLogModel *m_logModel; + ProtoOption *m_keyProto; + ProtoOption *m_valueProto; + + std::unique_ptr m_keyConverter; + std::unique_ptr m_valueConverter; }; /**! diff --git a/src/components/ProtoOption.cpp b/src/components/ProtoOption.cpp new file mode 100644 index 0000000..bb3cc5f --- /dev/null +++ b/src/components/ProtoOption.cpp @@ -0,0 +1,54 @@ +#include "spdlog/spdlog.h" + +#include "AbstractConverter.h" +#include "Consumer.h" +#include "KafkaConsumer.h" +#include "ProtoOption.h" + +#include "formats/protobuf/ProtobufConverter.h" + +ProtoOption::ProtoOption(QObject *parent) + : QObject(parent) +{} + +void ProtoOption::setFile(const QUrl &file) +{ + m_file = file; + emit changed(); +} + +const QUrl &ProtoOption::file() const +{ + return m_file; +} + +void ProtoOption::setMessage(const QString &message) +{ + m_message = message; + emit changed(); +} + +const QString &ProtoOption::message() const +{ + return m_message; +} + +std::tuple, ErrorWrap> ProtoOption::converter() +{ + if (!m_file.isValid()) { + return std::make_tuple(nullptr, ErrorWrap("converter", "path to proto not set")); + } + + if (m_message.isEmpty()) { + return std::make_tuple(nullptr, ErrorWrap("converter", "proto message not select")); + } + + QStringList errors; + auto converter = formats::protobuf::ProtobufConverter::fabric(m_file, m_message, errors); + if (converter == nullptr) { + spdlog::error("failed create protobuf converter"); + return std::make_tuple(nullptr, ErrorWrap("consumer", errors.join('\n'))); + } + + return std::make_tuple(std::move(converter), ErrorWrap{}); +} diff --git a/src/components/ProtoOption.h b/src/components/ProtoOption.h new file mode 100644 index 0000000..39073ff --- /dev/null +++ b/src/components/ProtoOption.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include +#include + +#include "ErrorWrap.h" + +namespace core { +class AbstractConverter; +} + +/**! + * ProtoOption protobuf options and fabric + */ +class ProtoOption : public QObject +{ + Q_OBJECT +public: + Q_PROPERTY(QUrl file READ file WRITE setFile) + Q_PROPERTY(QString message READ message WRITE setMessage) + + explicit ProtoOption(QObject *parent = nullptr); + + void setFile(const QUrl &file); + const QUrl &file() const; + + void setMessage(const QString &message); + const QString &message() const; + + std::tuple, ErrorWrap> converter(); + +signals: + + void changed(); + +private: + QUrl m_file; + QString m_message; +}; \ No newline at end of file diff --git a/src/formats/protobuf/ProtobufConverter.cpp b/src/formats/protobuf/ProtobufConverter.cpp index 441326b..11c2bce 100644 --- a/src/formats/protobuf/ProtobufConverter.cpp +++ b/src/formats/protobuf/ProtobufConverter.cpp @@ -12,7 +12,7 @@ #include "ProtobufSourceTree.h" namespace { -QByteArray errParse("\"failed parse message\""); +const QByteArray errParse("\"failed parse message\""); } namespace formats::protobuf { @@ -26,7 +26,7 @@ QByteArray ProtobufConverter::toJSON(QByteArray &&binary) return errParse; } - static JsonPrintOptions opt; + JsonPrintOptions opt; std::string json; MessageToJsonString(*m_message, &json, opt); return QByteArray(json.c_str(), json.size()); @@ -34,7 +34,22 @@ QByteArray ProtobufConverter::toJSON(QByteArray &&binary) QByteArray ProtobufConverter::fromJSON(QByteArray &&json) { - return std::move(json); + using namespace google::protobuf::util; + using namespace google::protobuf::stringpiece_internal; + + //m_message->Clear(); + + StringPiece piece(json.data(), json.size()); + + JsonParseOptions opt; + opt.ignore_unknown_fields = true; + JsonStringToMessage(piece, m_message.get(), opt); + const auto out = m_message->SerializeAsString(); + if (m_collector->hasErrors()) { + qDebug() << "Serialize errors"; + qDebug() << m_collector->errors(); + } + return QByteArray::fromStdString(out); } std::unique_ptr ProtobufConverter::fabric(const QUrl &file, diff --git a/src/formats/protobuf/ProtobufErrorCollector.cpp b/src/formats/protobuf/ProtobufErrorCollector.cpp index 7a1685e..7485418 100644 --- a/src/formats/protobuf/ProtobufErrorCollector.cpp +++ b/src/formats/protobuf/ProtobufErrorCollector.cpp @@ -1,3 +1,5 @@ +#include "spdlog/spdlog.h" + #include "ProtobufErrorCollector.h" namespace formats::protobuf { @@ -7,6 +9,7 @@ void ProtobufErrorCollector::AddError(const std::string &filename, int column, const std::string &message) { + spdlog::error("proto file: {}, line: {}, column: {} {}", filename, line, column, message); m_messages << QString("error file: %1, line: %2, column: %3 %4") .arg(QString::fromStdString(filename)) .arg(line) @@ -19,6 +22,7 @@ void ProtobufErrorCollector::AddWarning(const std::string &filename, int column, const std::string &message) { + spdlog::warn("proto file: {}, line: {}, column: {} {}", filename, line, column, message); m_messages << QString("warning file: %1, line: %2, column: %3 %4") .arg(QString::fromStdString(filename)) .arg(line) diff --git a/src/main.cpp b/src/main.cpp index bf4c02c..eb7d693 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include "ConfigModel.h" #include "Consumer.h" #include "ConsumerHelperModels.h" +#include "Helpers.h" #include "KafkaConnectivityTester.h" #include "Producer.h" #include "Registry.h" @@ -56,36 +57,12 @@ int main(int argc, char *argv[]) { Q_INIT_RESOURCE(protobuf); - qmlRegisterType("plumber", 1, 0, "ConnectivityTester"); - qmlRegisterType("plumber", 1, 0, "ConfigModel"); - qmlRegisterType("plumber", 1, 0, "Cluster"); - qmlRegisterType("plumber", 1, 0, "TopicFilterModel"); - qmlRegisterType("plumber", 1, 0, "Consumer"); - qmlRegisterType("plumber", 1, 0, "ConsumerTypeSelector"); - qmlRegisterType("plumber", 1, 0, "ConsumerLimiterSelector"); - qmlRegisterType("plumber", 1, 0, "ConsumerFilterSelector"); - qmlRegisterType("plumber", 1, 0, "ConsumerBeginningSelector"); - qmlRegisterType("plumber", 1, 0, "TopicCreator"); + registerTypes(); qmlRegisterType("plumber", 1, 0, "ProtobufMessagesModel"); - qmlRegisterType("plumber", 1, 0, "Producer"); - qmlRegisterType("plumber", 1, 0, "ProducerOptions"); - - qmlRegisterType("plumber", 1, 0, "ConsumerTypesModel"); - qmlRegisterType("plumber", 1, 0, "CustomTypesModel"); - qmlRegisterType("plumber", 1, 0, "StartFromTimeBasedModel"); - qmlRegisterType("plumber", 1, 0, "FiltersModel"); - qmlRegisterType("plumber", 1, 0, "LimitModel"); - qmlRegisterType("plumber", 1, 0, "MessageWrapper"); - qmlRegisterType("plumber", 1, 0, "HeaderTableModel"); - - qmlRegisterAnonymousType("plumber", 1); - qmlRegisterAnonymousType("plumber", 1); - qmlRegisterAnonymousType("plumber", 1); - kafka::KafkaClient::setGlobalLogger(KafkaSpdLogger); QCoreApplication::setApplicationName("plumber"); diff --git a/src/plumber_ru_RU.ts b/src/plumber_ru_RU.ts index b533af9..26678e2 100644 --- a/src/plumber_ru_RU.ts +++ b/src/plumber_ru_RU.ts @@ -4,17 +4,17 @@ Brokers - + BROKERS - + Brokers - + Listener @@ -95,13 +95,13 @@ - - + + START - + STOP @@ -109,47 +109,47 @@ CreateTopicDialog - + Create New Topic - + Name - + My new Topic name - + Partitions - + Replication Factor - + Cleanup Policy - + Retention (time or size) - + Compaction (key-based) - + CREATE TOPIC @@ -157,17 +157,17 @@ DataPage - + Topic: - + Key: - + Value: @@ -175,7 +175,7 @@ ErrorDialog - + Error @@ -183,7 +183,7 @@ FilterInput - + Simple @@ -191,22 +191,22 @@ FormatSelector - + ▼ FORMAT - + ▶ FORMAT - + Key - + Value @@ -214,7 +214,7 @@ LimitInput - + Global @@ -240,7 +240,7 @@ MessageTableView - + Details @@ -248,47 +248,47 @@ MessageView - + Record - + Data - + Headers - + Metadata - + Topic: - + Partition: - + Offset: - + Timestamp: - + Timestamp Type: @@ -319,17 +319,17 @@ - + Brokers - + TOPICS - + Topics @@ -350,22 +350,22 @@ ProducerScreen - + Produce to Topic: - + Data - + Options - + PRODUCE TO TOPIC @@ -373,27 +373,27 @@ RangeAndFilters - + ▼ RANGE & FILTERS - + ▶ RANGE & FILTERS - + Start From - + Limit - + Filter @@ -401,7 +401,7 @@ StartInput - + Time Based @@ -465,12 +465,12 @@ TypeCombobox - + Simple - + Custom diff --git a/src/qml.qrc b/src/qml.qrc index 17f5a5a..295d3fb 100644 --- a/src/qml.qrc +++ b/src/qml.qrc @@ -59,5 +59,6 @@ qml/Producer/DataPage.qml qml/Producer/OptionsPage.qml qml/Consumer/ProtobufOptions.qml + qml/Producer/ProtobufOptions.qml diff --git a/src/qml/BrokerDelegate.qml b/src/qml/BrokerDelegate.qml index 0721980..b1d7346 100644 --- a/src/qml/BrokerDelegate.qml +++ b/src/qml/BrokerDelegate.qml @@ -4,13 +4,13 @@ import QtQuick.Controls 2.12 Item { id: menuItem - property alias brokerColor: colorRect.color + property alias brokerColor: colorRect.color property string brokerName property string brokerBootstrap - signal remove - signal open + signal remove() + signal open() width: 150 height: 40 @@ -21,10 +21,12 @@ Item { Rectangle { id: colorRect + height: menuItem.height width: 4 color: brokerColor } + ColumnLayout { spacing: 2 Layout.fillWidth: true @@ -36,23 +38,31 @@ Item { font.pointSize: 12 Layout.fillWidth: true } + Text { id: name + text: brokerBootstrap clip: true Layout.fillWidth: true } + } + } + MouseArea { id: hoverArea + anchors.fill: parent hoverEnabled: true propagateComposedEvents: true onDoubleClicked: menuItem.open() } + Button { id: toolBotton + visible: hoverArea.containsMouse text: "☰" height: 20 @@ -65,11 +75,15 @@ Item { contextMenu.popup(); } } + Menu { id: contextMenu + MenuItem { text: "Removed" onTriggered: menuItem.remove() } + } + } diff --git a/src/qml/Brokers.qml b/src/qml/Brokers.qml index 6644db1..783e282 100644 --- a/src/qml/Brokers.qml +++ b/src/qml/Brokers.qml @@ -5,7 +5,9 @@ import "style.js" as Style Rectangle { id: item + property var brokerModel: mainCluster.brokerModel() + width: 300 height: 150 border.color: Style.BorderColor @@ -17,6 +19,7 @@ Rectangle { OverviewItem { Layout.fillWidth: true text: qsTr("BROKERS") + content: Item { RowLayout { anchors.fill: parent @@ -36,21 +39,30 @@ Rectangle { color: Style.LabelColor Layout.alignment: Qt.AlignCenter } + Text { id: name + font.pixelSize: 22 text: qsTr("Brokers") color: Style.LabelColor Layout.alignment: Qt.AlignCenter } + Item { Layout.fillHeight: true } + } + } + } + } + } + Item { Layout.fillWidth: true implicitWidth: 250 @@ -63,22 +75,27 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: 6 } + Rectangle { height: 1 width: parent.width anchors.bottom: parent.bottom color: "#f2f2f2" } + } + ListView { Layout.fillHeight: true Layout.fillWidth: true clip: true model: brokerModel boundsMovement: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + delegate: Item { height: 40 width: item.width @@ -90,13 +107,18 @@ Rectangle { text: node color: "#2a5fb0" } + Rectangle { height: 1 width: parent.width anchors.bottom: parent.bottom color: "#f2f2f2" } + } + } + } + } diff --git a/src/qml/Components/ErrorDialog.qml b/src/qml/Components/ErrorDialog.qml index 233d3e6..febeef4 100644 --- a/src/qml/Components/ErrorDialog.qml +++ b/src/qml/Components/ErrorDialog.qml @@ -4,6 +4,7 @@ import "../style.js" as Style Dialog { id: dialog + function show(e) { label.text = e.what; dialog.open(); @@ -15,8 +16,10 @@ Dialog { Text { id: label + anchors.fill: parent wrapMode: Text.WordWrap font.family: Style.FontFamily } + } diff --git a/src/qml/Components/MessageBox.qml b/src/qml/Components/MessageBox.qml index 2bcebea..c71b2d5 100644 --- a/src/qml/Components/MessageBox.qml +++ b/src/qml/Components/MessageBox.qml @@ -4,20 +4,24 @@ import "../style.js" as Style Dialog { id: dialog - property alias text: label.text + property alias text: label.text property var accaptHanlder: null + modal: true standardButtons: Dialog.Ok | Dialog.Cancel onAccepted: { if (accaptHanlder !== null) accaptHanlder(); + } Text { id: label + anchors.fill: parent wrapMode: Text.WordWrap font.family: Style.FontFamily } + } diff --git a/src/qml/Components/TextArea.qml b/src/qml/Components/TextArea.qml index 0841a79..dbb29cc 100644 --- a/src/qml/Components/TextArea.qml +++ b/src/qml/Components/TextArea.qml @@ -3,15 +3,21 @@ import QtQuick.Controls 6.0 Flickable { id: control + property alias text: area.text + TextArea.flickable: TextArea { id: area + persistentSelection: true selectByMouse: true + background: Rectangle { implicitHeight: control.width implicitWidth: 500 border.color: "#ababab" } + } + } diff --git a/src/qml/Components/TextButton.qml b/src/qml/Components/TextButton.qml index 87402a4..421bc99 100644 --- a/src/qml/Components/TextButton.qml +++ b/src/qml/Components/TextButton.qml @@ -3,13 +3,15 @@ import "../style.js" as Style Text { id: label - signal clickecd + + signal clickecd() color: Style.LabelColorDark font.underline: mouseArea.containsMouse MouseArea { id: mouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -17,4 +19,5 @@ Text { label.clickecd(); } } + } diff --git a/src/qml/Components/TypeCombobox.qml b/src/qml/Components/TypeCombobox.qml index b9a55d9..9d1d792 100644 --- a/src/qml/Components/TypeCombobox.qml +++ b/src/qml/Components/TypeCombobox.qml @@ -6,9 +6,10 @@ import "../style.js" as Style Item { id: item - property string typeName: "string" + property string typeName: "string" property int typeId: 1 + implicitHeight: label.height implicitWidth: label.width @@ -17,19 +18,25 @@ Item { Text { id: label + text: item.typeName color: Style.LabelColorDark font.underline: mouseArea.containsMouse } + Text { id: icon + text: "▼" font.pointSize: 6 color: "#d4d4d4" } + } + MouseArea { id: mouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -37,8 +44,10 @@ Item { popup.open(); } } + Popup { id: popup + focus: true width: 250 @@ -51,11 +60,13 @@ Item { color: Style.LabelColor font.bold: true } + Rectangle { width: popup.width / 2 - 4 height: 1 color: Style.BorderColor } + Repeater { TextButton { text: typeName @@ -68,8 +79,11 @@ Item { model: ConsumerTypesModel { } + } + } + Column { spacing: 2 @@ -78,11 +92,13 @@ Item { color: Style.LabelColor font.bold: true } + Rectangle { width: popup.width / 2 - 4 height: 1 color: Style.BorderColor } + Repeater { TextButton { text: typeName @@ -95,13 +111,18 @@ Item { model: CustomTypesModel { } + } + } + } background: Rectangle { border.color: Style.BorderColor radius: 4 } + } + } diff --git a/src/qml/Consumer/ConsumerScreen.qml b/src/qml/Consumer/ConsumerScreen.qml index 72d0ad2..1f80b19 100644 --- a/src/qml/Consumer/ConsumerScreen.qml +++ b/src/qml/Consumer/ConsumerScreen.qml @@ -9,8 +9,8 @@ import "../Components" as Components Window { id: window - property string topic: "" + property string topic: "" property var topicModel property var broker @@ -25,26 +25,29 @@ Window { Consumer { id: consumer + topic: window.topic broker: window.broker + beginning: ConsumerBeginningSelector { startFrom: filter.startFromTimeBase specificDate: filter.specificDateText } + types: ConsumerTypeSelector { keyType: format.keyTypeId valueType: format.valueTypeId - keyProtoFile: format.keyProtoFile - keyProtoMessage: format.keyProtoMessage - valueProtoFile: format.valueProtoFile - valueProtoMessage: format.valueProtoMessage + protoKey: format.keyProto + protoValue: format.valueProto } + limiter: ConsumerLimiterSelector { limit: filter.selectedLimit numberOfRecords: filter.numberOfRecords maxSize: filter.maxSize specificDate: filter.limitSpecificDateText } + filter: ConsumerFilterSelector { filters: filter.selectedFilter key: filter.keyFilter @@ -52,13 +55,16 @@ Window { headerKey: filter.headerKeyFilter headerValue: filter.headerValueFilter } + } + RowLayout { anchors.fill: parent spacing: 0 Rectangle { id: leftPanel + Layout.fillHeight: true Layout.minimumWidth: 330 Layout.maximumWidth: 330 @@ -69,6 +75,7 @@ Window { ComboBox { id: topicBox + textRole: "topic" valueRole: "topic" Layout.fillWidth: true @@ -79,29 +86,40 @@ Window { Component.onCompleted: { if (window.topic !== "") currentIndex = indexOfValue(window.topic); + } + background: Rectangle { implicitWidth: 330 implicitHeight: 30 border.color: Style.BorderColor } + } + FormatSelector { id: format + Layout.fillWidth: true Layout.leftMargin: 6 } + RangeAndFilters { id: filter + Layout.fillWidth: true Layout.leftMargin: 6 } + Item { Layout.fillHeight: true Layout.fillWidth: true } + } + } + ColumnLayout { spacing: 0 Layout.fillHeight: true @@ -109,6 +127,7 @@ Window { MessageTableView { id: table + width: 627 height: Constants.ConsumerScreenHeight / 2 Layout.fillHeight: true @@ -116,6 +135,7 @@ Window { model: consumer.messages visibleHeader: tableBtn.checked } + Item { Layout.preferredHeight: 40 Layout.maximumHeight: 40 @@ -135,13 +155,16 @@ Window { leftPanel.visible = !leftPanel.visible; } } + Button { id: tableBtn + icon.source: "qrc:/table.svg" implicitWidth: 28 implicitHeight: 28 checkable: true } + Button { visible: tableBtn.checked icon.source: "qrc:/list.svg" @@ -151,6 +174,7 @@ Window { Popup { id: columnPopup + y: -implicitHeight + parent.height / 2 width: 150 focus: true @@ -169,33 +193,42 @@ Window { table.hideColumn(index, checked); } } + } + } background: Rectangle { border.color: Style.BorderColor radius: 4 } + } + } + Item { Layout.fillWidth: true } + Text { text: consumer.stat } + Button { id: start + text: qsTr("START") state: consumer.isRunning ? "stop" : "start" onClicked: { if (state === "start") { let err = consumer.start(); - if (err.isError) { + if (err.isError) consumerErrDialog.show(err); - } - } else + + } else { consumer.stop(); + } } states: [ State { @@ -205,6 +238,7 @@ Window { target: start text: qsTr("START") } + }, State { name: "stop" @@ -213,16 +247,24 @@ Window { target: start text: qsTr("STOP") } + } ] } + } + } + } + } + Components.ErrorDialog { id: consumerErrDialog + anchors.centerIn: parent width: parent.width * 0.8 } + } diff --git a/src/qml/Consumer/FilterInput.qml b/src/qml/Consumer/FilterInput.qml index c169b25..5a57633 100644 --- a/src/qml/Consumer/FilterInput.qml +++ b/src/qml/Consumer/FilterInput.qml @@ -7,29 +7,35 @@ import "../Components" as Components Item { id: item - property string selectedText: "(none)" + property string selectedText: "(none)" property var selectedFilterId: 0 + implicitHeight: label.height implicitWidth: label.width + icon.width + 6 Text { id: label + anchors.left: parent.left text: item.selectedText color: Style.LabelColorDark font.underline: mouseArea.containsMouse } + Text { id: icon + anchors.left: label.right anchors.verticalCenter: label.verticalCenter text: "▼" font.pointSize: 6 color: "#d4d4d4" } + MouseArea { id: mouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -38,8 +44,10 @@ Item { popup.open(); } } + Popup { id: popup + focus: true width: 400 @@ -51,11 +59,13 @@ Item { color: Style.LabelColor font.bold: true } + Rectangle { width: popup.width / 2 height: 1 color: Style.BorderColor } + Repeater { Components.TextButton { text: filterLabel @@ -68,12 +78,16 @@ Item { model: FiltersModel { } + } + } background: Rectangle { border.color: Style.BorderColor radius: 4 } + } + } diff --git a/src/qml/Consumer/FormatSelector.qml b/src/qml/Consumer/FormatSelector.qml index b5cefcd..ff7316b 100644 --- a/src/qml/Consumer/FormatSelector.qml +++ b/src/qml/Consumer/FormatSelector.qml @@ -1,29 +1,40 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import plumber 1.0 import "../style.js" as Style import "../Components" Item { property alias keyTypeId: keyCbx.typeId - property alias valueTypeId: valueCbx.typeId + property alias keyProto: protoKeyId + property alias valueProto: protoValueId - property alias keyProtoFile: protobufOptKey.protoFile - - property alias keyProtoMessage: protobufOptKey.protoMessage - - property alias valueProtoFile: protobufOptValue.protoFile - - property alias valueProtoMessage: protobufOptValue.protoMessage width: 320 implicitHeight: content.height implicitWidth: content.width + ProtoOption { + id: protoKeyId + + file: protobufOptKey.protoFile + message: protobufOptKey.protoMessage + } + + ProtoOption { + id: protoValueId + + file: protobufOptValue.protoFile + message: protobufOptValue.protoMessage + } + ColumnLayout { id: content + Text { id: label + text: contentGrid.visible ? qsTr("▼ FORMAT") : qsTr("▶ FORMAT") color: Style.LabelColor font.bold: true @@ -35,9 +46,12 @@ Item { contentGrid.visible = !contentGrid.visible; } } + } + ColumnLayout { id: contentGrid + GridLayout { columns: 2 @@ -46,16 +60,21 @@ Item { text: qsTr("Key") color: Style.LabelColor } + TypeCombobox { id: keyCbx } + } + ProtobufOptions { id: protobufOptKey + visible: keyCbx.typeId == 7 Layout.columnSpan: 2 Layout.preferredWidth: 320 } + GridLayout { columns: 2 @@ -64,16 +83,23 @@ Item { text: qsTr("Value") color: Style.LabelColor } + TypeCombobox { id: valueCbx } + } + ProtobufOptions { id: protobufOptValue + visible: valueCbx.typeId == 7 Layout.columnSpan: 2 Layout.preferredWidth: 320 } + } + } + } diff --git a/src/qml/Consumer/LimitInput.qml b/src/qml/Consumer/LimitInput.qml index baba382..6a6fbf7 100644 --- a/src/qml/Consumer/LimitInput.qml +++ b/src/qml/Consumer/LimitInput.qml @@ -7,20 +7,19 @@ import "../Components" as Components Item { id: item - property string selectedText: "None (forever)" + property string selectedText: "None (forever)" property int selectedLimit: 0 - property alias numberOfRecords: records.value - property alias maxSize: size.value - property alias specificDateText: specificDate.text + implicitHeight: layout.implicitHeight implicitWidth: layout.implicitWidth ColumnLayout { id: layout + spacing: 6 Item { @@ -29,24 +28,31 @@ Item { Row { id: labelRow + spacing: 6 Text { id: label + text: item.selectedText color: Style.LabelColorDark font.underline: mouseArea.containsMouse } + Text { id: icon + text: "▼" font.pointSize: 6 color: "#d4d4d4" anchors.verticalCenter: label.verticalCenter } + } + MouseArea { id: mouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -55,9 +61,12 @@ Item { popup.open(); } } + } + SpinBox { id: records + visible: item.selectedLimit == 1 from: 1 to: 1000 @@ -65,8 +74,10 @@ Item { stepSize: 1 Layout.preferredWidth: 230 } + TextField { id: specificDate + function setupDate() { let dt = new Date(); let timeString = dt.toLocaleString(Qt.locale(), "yyyy-MM-dd hh:mm:ss"); @@ -79,8 +90,10 @@ Item { persistentSelection: true selectByMouse: true } + SpinBox { id: size + visible: item.selectedLimit == 3 from: 0 to: 1024 * 1024 * 1024 @@ -88,9 +101,12 @@ Item { stepSize: 1 Layout.preferredWidth: 230 } + } + Popup { id: popup + focus: true width: 400 @@ -102,11 +118,13 @@ Item { color: Style.LabelColor font.bold: true } + Rectangle { width: popup.width / 2 height: 1 color: Style.BorderColor } + Repeater { Components.TextButton { text: limitLabel @@ -115,18 +133,23 @@ Item { item.selectedLimit = limitId; if (limitId === 2) specificDate.setupDate(); + popup.close(); } } model: LimitModel { } + } + } background: Rectangle { border.color: Style.BorderColor radius: 4 } + } + } diff --git a/src/qml/Consumer/MessageTableView.qml b/src/qml/Consumer/MessageTableView.qml index f67ea7f..e78a030 100644 --- a/src/qml/Consumer/MessageTableView.qml +++ b/src/qml/Consumer/MessageTableView.qml @@ -6,15 +6,12 @@ import "../pages.js" as Pages Rectangle { id: main + property var model property var columnVisible: [true, true, true, true, true, true, true] - property var columnWidths: [100, 50, 100, 150, 100, 350, 200] - property bool visibleHeader: false - property int rowHeight: 40 - property int fontPixelSize: 14 function columnWidthProvider(column) { @@ -22,6 +19,7 @@ Rectangle { let width = visible ? columnWidths[column] : 0; return width; } + function hideColumn(column, hide) { columnVisible[column] = hide; view.forceLayout(); @@ -37,12 +35,15 @@ Rectangle { model: main.model Layout.fillWidth: true Layout.fillHeight: true + ScrollBar.vertical: ScrollBar { id: listVerticalBar + policy: ScrollBar.AsNeeded minimumSize: 0.06 onPositionChanged: tableVerticalBar.position = position } + delegate: Rectangle { width: main.width implicitWidth: 100 @@ -66,6 +67,7 @@ Rectangle { font.family: Style.FontFamily color: Style.MessageGrayColor } + Text { text: "#" font.italic: true @@ -73,13 +75,16 @@ Rectangle { font.pixelSize: fontPixelSize font.family: Style.FontFamily } + Text { text: model.key font.pixelSize: fontPixelSize font.family: Style.FontFamily color: Style.MessageGrayColor } + } + Text { Layout.fillWidth: true Layout.fillHeight: true @@ -89,15 +94,19 @@ Rectangle { font.pixelSize: fontPixelSize font.family: Style.FontFamily } + } + MouseArea { id: hover + anchors.fill: parent hoverEnabled: true onDoubleClicked: { Pages.createMessageScreen(window.x, window.y, window.width, window.height, main.model.getMessage(index)); } } + RowLayout { anchors.fill: parent spacing: 8 @@ -107,6 +116,7 @@ Rectangle { Item { Layout.fillWidth: true } + Button { icon.source: "qrc:/arrows-expand.svg" text: qsTr("Details") @@ -114,9 +124,13 @@ Rectangle { Pages.createMessageScreen(window.x, window.y, window.width, window.height, main.model.getMessage(index)); } } + } + } + } + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true @@ -124,6 +138,7 @@ Rectangle { HorizontalHeaderView { id: horizontalHeader + reuseItems: false syncView: view height: 30 @@ -131,6 +146,7 @@ Rectangle { MouseArea { id: mouseArea + anchors.fill: parent propagateComposedEvents: true hoverEnabled: true @@ -138,6 +154,7 @@ Rectangle { delegate: Rectangle { id: root + implicitWidth: 50 implicitHeight: 30 @@ -147,8 +164,10 @@ Rectangle { color: Style.LabelColor font.bold: true } + Rectangle { id: splitter + color: Style.BorderColor height: parent.height width: 1 @@ -163,21 +182,28 @@ Rectangle { DragHandler { id: drag + yAxis.enabled: false xAxis.enabled: true cursorShape: Qt.SizeHorCursor } + } + } + } + TableView { id: view + Layout.fillWidth: true Layout.fillHeight: true columnWidthProvider: main.columnWidthProvider visible: true clip: true model: main.model + delegate: Item { implicitWidth: 100 implicitHeight: rowHeight @@ -198,6 +224,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } + Text { // part text: display @@ -209,6 +236,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } + Text { // offset text: display @@ -220,6 +248,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } + Text { // timestamp text: display @@ -231,6 +260,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } + Text { // key text: display @@ -241,6 +271,7 @@ Rectangle { elide: Text.ElideRight verticalAlignment: Text.AlignVCenter } + Text { // value text: display @@ -253,6 +284,7 @@ Rectangle { color: Style.MessageBlueColor verticalAlignment: Text.AlignVCenter } + Text { // headers text: display @@ -265,37 +297,49 @@ Rectangle { verticalAlignment: Text.AlignVCenter wrapMode: Text.WordWrap } + } + Rectangle { height: 1 width: parent.width anchors.bottom: parent.bottom color: "#f2f2f2" } + MouseArea { anchors.fill: parent onDoubleClicked: { Pages.createMessageScreen(window.x, window.y, window.width, window.height, main.model.getMessage(row)); } } + } + ScrollBar.vertical: ScrollBar { id: tableVerticalBar + policy: ScrollBar.AsNeeded minimumSize: 0.06 onPositionChanged: listVerticalBar.position = position } + ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded minimumSize: 0.06 } + } + } + } + Rectangle { anchors.bottom: parent.bottom width: parent.width height: 1 color: Style.BorderColor } + } diff --git a/src/qml/Consumer/MessageView.qml b/src/qml/Consumer/MessageView.qml index d030657..cd4bc52 100644 --- a/src/qml/Consumer/MessageView.qml +++ b/src/qml/Consumer/MessageView.qml @@ -7,6 +7,7 @@ import "../constants.js" as Constants Window { id: window + property var message visible: true @@ -16,12 +17,16 @@ Window { MessageWrapper { id: msg + message: window.message } + HeaderTableModel { id: headers + message: window.message } + Rectangle { anchors.fill: parent color: "#ffffff" @@ -33,21 +38,26 @@ Window { TabBar { id: bar + Layout.fillWidth: true TabButton { width: implicitWidth + 16 text: qsTr("Data") } + TabButton { width: implicitWidth + 16 text: qsTr("Headers") } + TabButton { width: implicitWidth + 16 text: qsTr("Metadata") } + } + StackLayout { Layout.fillWidth: true Layout.fillHeight: true @@ -70,6 +80,7 @@ Window { selectByMouse: true persistentSelection: true } + ScrollView { Layout.fillWidth: true Layout.fillHeight: true @@ -80,15 +91,21 @@ Window { readOnly: true selectByMouse: true persistentSelection: true + background: Rectangle { implicitHeight: 150 implicitWidth: 500 border.color: "#ababab" } + } + } + } + } + TableView { width: 100 height: 100 @@ -96,6 +113,7 @@ Window { Layout.fillWidth: true model: headers clip: true + delegate: Rectangle { implicitWidth: Constants.MessageViewWidth / 2 implicitHeight: 40 @@ -109,14 +127,18 @@ Window { verticalAlignment: Text.AlignVCenter wrapMode: Text.WordWrap } + Rectangle { height: 1 width: parent.width anchors.bottom: parent.bottom color: "#f2f2f2" } + } + } + Item { Layout.fillHeight: true Layout.fillWidth: true @@ -131,40 +153,56 @@ Window { Text { text: qsTr("Topic:") } + Text { text: msg.topic } + Text { text: qsTr("Partition:") } + Text { text: msg.partition } + Text { text: qsTr("Offset:") } + Text { text: msg.offset } + Text { text: qsTr("Timestamp:") } + Text { text: msg.timestamp } + Text { text: qsTr("Timestamp Type:") } + Text { text: msg.timestampType } + Item { Layout.fillHeight: true Layout.columnSpan: 2 } + } + } + } + } + } + } diff --git a/src/qml/Consumer/ProtobufOptions.qml b/src/qml/Consumer/ProtobufOptions.qml index 430a39d..6a0f823 100644 --- a/src/qml/Consumer/ProtobufOptions.qml +++ b/src/qml/Consumer/ProtobufOptions.qml @@ -7,14 +7,16 @@ import "../style.js" as Style Item { id: itm - width: 250 - implicitHeight: content.height property url protoFile: fileDialog.file property alias protoMessage: messageCbx.currentText + width: 250 + implicitHeight: content.height + ColumnLayout { id: content + width: parent.width spacing: 4 @@ -26,46 +28,58 @@ Item { text: "Proto:" color: Style.LabelColor } + Text { id: path + Layout.fillWidth: true elide: Text.ElideLeft horizontalAlignment: Text.AlignRight text: fileDialog.file color: Style.LabelColor } + Button { text: "..." width: 38 implicitWidth: 38 onClicked: fileDialog.open() } + } + ComboBox { id: messageCbx + Layout.fillWidth: true model: protoMessages textRole: "message" } + } + FileDialog { id: fileDialog + nameFilters: ["Proto files (*.proto)"] onFileChanged: { itm.protoFile = file; } } + ProtobufMessagesModel { id: protoMessages + file: fileDialog.file - onLoaded: { - messageCbx.currentIndex = 0; - } - onParseErrors: errs => { + onParseErrors: (errs) => { let e = { "what": errs }; consumerErrDialog.show(e); } + onLoaded: { + messageCbx.currentIndex = 0; + } } + } diff --git a/src/qml/Consumer/RangeAndFilters.qml b/src/qml/Consumer/RangeAndFilters.qml index 92073bf..551e047 100644 --- a/src/qml/Consumer/RangeAndFilters.qml +++ b/src/qml/Consumer/RangeAndFilters.qml @@ -5,26 +5,17 @@ import "../style.js" as Style Item { id: item - property alias headerKeyFilter: headerKey.text + property alias headerKeyFilter: headerKey.text property alias headerValueFilter: headerValue.text - property alias keyFilter: key.text - property alias valueFilter: value.text - property alias selectedFilter: filterSelector.selectedFilterId - property alias startFromTimeBase: startFrom.selectedTimeBase - property alias specificDateText: startFrom.specificDateText - property alias selectedLimit: limit.selectedLimit - property alias numberOfRecords: limit.numberOfRecords - property alias maxSize: limit.maxSize - property alias limitSpecificDateText: limit.specificDateText function resize() { @@ -37,8 +28,10 @@ Item { ColumnLayout { id: content + Text { id: label + text: contentGrid.visible ? qsTr("▼ RANGE & FILTERS") : qsTr("▶ RANGE & FILTERS") color: Style.LabelColor font.bold: true @@ -51,9 +44,12 @@ Item { resize(); } } + } + GridLayout { id: contentGrid + Layout.fillWidth: true columns: 2 @@ -63,38 +59,49 @@ Item { color: Style.LabelColor Layout.alignment: Qt.AlignLeft | Qt.AlignTop } + StartInput { id: startFrom + Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.minimumWidth: 80 onSelectedTimeBaseChanged: resize() } + Text { Layout.minimumWidth: 70 text: qsTr("Limit") color: Style.LabelColor Layout.alignment: Qt.AlignLeft | Qt.AlignTop } + LimitInput { id: limit + Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.minimumWidth: 80 } + Text { Layout.minimumWidth: 70 text: qsTr("Filter") color: Style.LabelColor Layout.alignment: Qt.AlignLeft | Qt.AlignTop } + FilterInput { id: filterSelector + Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.minimumWidth: 80 onSelectedFilterIdChanged: resize() } + } + Grid { id: simpleFilterValues + columns: 2 visible: false spacing: 6 @@ -102,40 +109,54 @@ Item { Text { text: "Key?" } + TextField { id: key + placeholderText: "A key to match. Optional" persistentSelection: true selectByMouse: true } + Text { text: "Value?" } + TextField { id: value + placeholderText: "A value to match. Optional" persistentSelection: true selectByMouse: true } + Text { text: "Header?" } + Row { spacing: 6 TextField { id: headerKey + placeholderText: "Key. Optional" persistentSelection: true selectByMouse: true } + TextField { id: headerValue + placeholderText: "Value. Optional" persistentSelection: true selectByMouse: true } + } + } + } + } diff --git a/src/qml/Consumer/StartInput.qml b/src/qml/Consumer/StartInput.qml index f509226..1a022e0 100644 --- a/src/qml/Consumer/StartInput.qml +++ b/src/qml/Consumer/StartInput.qml @@ -7,16 +7,17 @@ import "../Components" as Components Item { id: item - property string selectedText: "Now (latest)" + property string selectedText: "Now (latest)" property int selectedTimeBase: 0 - property alias specificDateText: specificDate.text + implicitHeight: layout.implicitHeight implicitWidth: layout.implicitWidth ColumnLayout { id: layout + spacing: 6 Item { @@ -25,24 +26,31 @@ Item { Row { id: labelRow + spacing: 6 Text { id: label + text: item.selectedText color: Style.LabelColorDark font.underline: mouseArea.containsMouse } + Text { id: icon + text: "▼" font.pointSize: 6 color: "#d4d4d4" anchors.verticalCenter: label.verticalCenter } + } + MouseArea { id: mouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -51,9 +59,12 @@ Item { popup.open(); } } + } + TextField { id: specificDate + function setupDate() { let dt = new Date(); let timeString = dt.toLocaleString(Qt.locale(), "yyyy-MM-dd hh:mm:ss"); @@ -66,9 +77,12 @@ Item { width: 150 Layout.minimumWidth: 150 } + } + Popup { id: popup + focus: true width: 400 @@ -80,11 +94,13 @@ Item { color: Style.LabelColor font.bold: true } + Rectangle { width: popup.width / 2 height: 1 color: Style.BorderColor } + Repeater { Components.TextButton { text: startTimeLabel @@ -99,12 +115,16 @@ Item { model: StartFromTimeBasedModel { } + } + } background: Rectangle { border.color: Style.BorderColor radius: 4 } + } + } diff --git a/src/qml/Consumers.qml b/src/qml/Consumers.qml index 8423c66..b79b519 100644 --- a/src/qml/Consumers.qml +++ b/src/qml/Consumers.qml @@ -5,6 +5,7 @@ import "style.js" as Style Rectangle { id: item + width: 300 height: 150 border.color: Style.BorderColor diff --git a/src/qml/CreateTopicDialog.qml b/src/qml/CreateTopicDialog.qml index 13030de..90f8caf 100644 --- a/src/qml/CreateTopicDialog.qml +++ b/src/qml/CreateTopicDialog.qml @@ -8,7 +8,9 @@ import "Components" as Components Window { id: dialog + property alias broker: topic.broker + visible: true height: 280 width: 800 @@ -16,12 +18,14 @@ Window { TopicCreator { id: topic + name: name.text partitions: partitions.value replicationFactor: replication.value retention: retention.checked compaction: compaction.checked } + Page { anchors.fill: parent @@ -37,65 +41,86 @@ Window { text: qsTr("Name") Layout.preferredWidth: 130 } + TextField { id: name + Layout.fillWidth: true placeholderText: qsTr("My new Topic name") selectByMouse: true } + Text { text: qsTr("Partitions") } + SpinBox { id: partitions + from: 1 stepSize: 1 value: 3 Layout.preferredWidth: 60 } + Text { text: qsTr("Replication Factor") } + SpinBox { id: replication + from: 1 to: mainCluster.brokerModel().brokers Layout.preferredWidth: 60 stepSize: 1 value: 1 } + Text { text: qsTr("Cleanup Policy") } + RowLayout { Text { text: qsTr("Retention (time or size)") } + Switch { id: retention + checked: true onCheckedChanged: { if (!retention.checked) compaction.checked = true; + } } + Text { text: qsTr("Compaction (key-based)") } + Switch { id: compaction + onCheckedChanged: { if (!compaction.checked) retention.checked = true; + } } + } + Item { Layout.columnSpan: 2 Layout.fillWidth: true Layout.fillHeight: true } + } + } footer: Rectangle { @@ -109,26 +134,34 @@ Window { Item { Layout.fillWidth: true } + Button { id: createButton + enabled: name.length > 0 text: qsTr("CREATE TOPIC") onClicked: { let e = topic.create(); if (e.isError) { err.show(e); - return; + return ; } mainCluster.topicModel().refresh(); dialog.close(); } } + } + } + } + Components.ErrorDialog { id: err + anchors.centerIn: parent width: parent.width * 0.8 } + } diff --git a/src/qml/LeftPanel/MenuItem.qml b/src/qml/LeftPanel/MenuItem.qml index 2896d24..cac87d9 100644 --- a/src/qml/LeftPanel/MenuItem.qml +++ b/src/qml/LeftPanel/MenuItem.qml @@ -4,10 +4,9 @@ import "../style.js" as Style Item { id: itm - property int index: 0 + property int index: 0 property string text: "" - property string icon: "" signal clicked(int index) @@ -18,9 +17,11 @@ Item { Rectangle { id: bg + anchors.fill: parent anchors.margins: 1 } + RowLayout { anchors.fill: parent spacing: 6 @@ -35,17 +36,22 @@ Item { sourceSize.width: 25 sourceSize.height: 25 } + } + Text { text: itm.text color: Style.LabelColor font.pixelSize: 24 Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft } + Item { Layout.fillWidth: true } + } + MouseArea { anchors.fill: parent onClicked: { @@ -62,6 +68,7 @@ Item { target: bg color: "#ffffff" } + }, State { name: "checked" @@ -70,6 +77,7 @@ Item { target: bg color: "#f5f5f5" } + } ] } diff --git a/src/qml/Overview.qml b/src/qml/Overview.qml index a6f09c7..6fd6f19 100644 --- a/src/qml/Overview.qml +++ b/src/qml/Overview.qml @@ -6,8 +6,8 @@ import "constants.js" as Constants Item { id: overview - property var brokerModel: mainCluster.brokerModel() + property var brokerModel: mainCluster.brokerModel() property var topicModel: mainCluster.topicModel() signal activatedItem(int indx) @@ -20,6 +20,7 @@ Item { Layout.fillWidth: true text: qsTr("BROKERS") onClicked: overview.activatedItem(Constants.BrokersIndex) + content: Item { RowLayout { anchors.fill: parent @@ -39,24 +40,33 @@ Item { color: Style.LabelColor Layout.alignment: Qt.AlignCenter } + Text { font.pixelSize: 22 text: qsTr("Brokers") color: Style.LabelColor Layout.alignment: Qt.AlignCenter } + Item { Layout.fillHeight: true } + } + } + } + } + } + OverviewItem { Layout.fillWidth: true text: qsTr("TOPICS") onClicked: overview.activatedItem(Constants.TopicsIndex) + content: Item { RowLayout { anchors.fill: parent @@ -76,19 +86,26 @@ Item { color: Style.LabelColor Layout.alignment: Qt.AlignCenter } + Text { font.pixelSize: 22 text: qsTr("Topics") color: Style.LabelColor Layout.alignment: Qt.AlignCenter } + Item { Layout.fillHeight: true } + } + } + } + } + } /* OverviewItem { @@ -102,9 +119,12 @@ Item { 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 232305b..ed469d2 100644 --- a/src/qml/OverviewItem.qml +++ b/src/qml/OverviewItem.qml @@ -4,11 +4,11 @@ import "style.js" as Style Rectangle { id: item - property alias text: label.text + property alias text: label.text property alias content: loader.sourceComponent - signal clicked + signal clicked() width: 300 height: 130 @@ -20,6 +20,7 @@ Rectangle { Text { id: label + text: item.label font.pixelSize: 24 color: Style.LabelColorDark @@ -27,16 +28,22 @@ Rectangle { MouseArea { id: mouseArea + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: item.clicked() } + } + Loader { id: loader + Layout.fillHeight: true Layout.fillWidth: true } + } + } diff --git a/src/qml/Producer/DataPage.qml b/src/qml/Producer/DataPage.qml index 4b5cee3..32e2fe5 100644 --- a/src/qml/Producer/DataPage.qml +++ b/src/qml/Producer/DataPage.qml @@ -7,29 +7,54 @@ import "../Components" as Components Item { property var topicModel property alias selectedTopic: topicBox.currentValue - property alias keyTypeId: keyCbx.typeId - property alias valueTypeId: valueCbx.typeId + property alias protoKey: protoKeyId + property alias protoValue: protoValueId function keyData() { - if (keyCbx.typeId === 6) + switch (keyCbx.typeId) { + case 0: + return keyArea.text; + case 6: return ""; - if (keyCbx.typeId !== 0) + case 7: + return keyArea.text; + default: return keyTxt.text; - return keyArea.text; + } } + function valueData() { - if (valueCbx.typeId === 6) + switch (valueCbx.typeId) { + case 0: + return valueArea.text; + case 6: return ""; - if (valueCbx.typeId !== 0) + case 7: + return valueArea.text; + default: return valueTxt.text; - return valueArea.text; + } } height: 100 width: 100 + ProtoOption { + id: protoKeyId + + file: keyProto.protoFile + message: keyProto.protoMessage + } + + ProtoOption { + id: protoValueId + + file: valueProto.protoFile + message: valueProto.protoMessage + } + GridLayout { anchors.fill: parent anchors.margins: 8 @@ -39,63 +64,97 @@ Item { Text { text: qsTr("Topic:") } + ComboBox { id: topicBox + model: topicModel textRole: "topic" valueRole: "topic" Layout.preferredWidth: 300 } + Text { text: qsTr("Key:") } + Components.TypeCombobox { id: keyCbx } + TextField { id: keyTxt + Layout.columnSpan: 2 Layout.fillWidth: true selectByMouse: true visible: keyCbx.typeId >= 1 && keyCbx.typeId < 6 } + + ProtobufOptions { + id: keyProto + + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: keyCbx.typeId === 7 + } + Components.TextArea { id: keyArea + Layout.columnSpan: 2 Layout.fillWidth: true Layout.preferredHeight: 200 - visible: keyCbx.typeId === 0 + visible: keyCbx.typeId === 0 || keyCbx.typeId === 7 } + Rectangle { Layout.columnSpan: 2 Layout.fillWidth: true height: 1 color: "#f0f0f0" } + Text { text: qsTr("Value:") } + Components.TypeCombobox { id: valueCbx } + TextField { id: valueTxt + Layout.columnSpan: 2 Layout.fillWidth: true selectByMouse: true visible: valueCbx.typeId >= 1 && valueCbx.typeId < 6 } + + ProtobufOptions { + id: valueProto + + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: valueCbx.typeId === 7 + } + Components.TextArea { id: valueArea + Layout.columnSpan: 2 Layout.fillWidth: true Layout.fillHeight: true - visible: valueCbx.typeId === 0 + visible: valueCbx.typeId === 0 || valueCbx.typeId === 7 } + Item { - visible: valueCbx.typeId !== 0 + visible: !valueArea.visible Layout.columnSpan: 2 Layout.fillHeight: true } + } + } diff --git a/src/qml/Producer/ProducerScreen.qml b/src/qml/Producer/ProducerScreen.qml index ba4b6de..7c34533 100644 --- a/src/qml/Producer/ProducerScreen.qml +++ b/src/qml/Producer/ProducerScreen.qml @@ -10,6 +10,7 @@ import "../Components" as Components Window { //color: Style.Background id: window + property var topicModel property var broker @@ -20,16 +21,22 @@ Window { Producer { id: producer + keyType: dataPage.keyTypeId valueType: dataPage.valueTypeId topic: dataPage.selectedTopic broker: window.broker + protoKey: dataPage.protoKey + protoValue: dataPage.protoValue + options: ProducerOptions { compression: optionsPage.compression ack: optionsPage.ack idempotence: optionsPage.idempotence } + } + SplitView { anchors.fill: parent @@ -39,6 +46,7 @@ Window { SplitView.maximumWidth: 400 SplitView.fillHeight: true } + Item { SplitView.fillHeight: true SplitView.fillWidth: true @@ -55,9 +63,10 @@ Window { TabBar { id: bar - property int buttonWidth: 130 + property int buttonWidth: 130 property int buttonheight: 25 + anchors.top: parent.top width: parent.width height: 24 @@ -67,12 +76,15 @@ Window { width: bar.buttonWidth height: bar.buttonheight } + TabButton { text: qsTr("Options") width: bar.buttonWidth height: bar.buttonheight } + } + StackLayout { width: parent.width anchors.top: bar.bottom @@ -81,17 +93,23 @@ Window { DataPage { id: dataPage + Layout.fillHeight: true Layout.fillWidth: true topicModel: window.topicModel } + OptionsPage { id: optionsPage + Layout.fillHeight: true Layout.fillWidth: true } + } + } + Rectangle { width: 250 height: 100 @@ -104,6 +122,7 @@ Window { anchors.fill: parent anchors.margins: 1 model: producer.log + delegate: Rectangle { width: parent.width height: 68 @@ -116,24 +135,34 @@ Window { Text { text: timestamp } + Text { text: "> " + topic + "-" + partition } + Text { text: "Offset: " + offset } + Item { Layout.fillHeight: true } + } + MouseArea { id: hover + anchors.fill: parent hoverEnabled: true } + } + } + } + Item { Layout.columnSpan: 2 Layout.fillWidth: true @@ -146,19 +175,24 @@ Window { Item { Layout.fillWidth: true } + Button { text: qsTr("PRODUCE TO TOPIC") onClicked: { let e = producer.send(dataPage.keyData(), dataPage.valueData()); if (e.isError) { err.show(e); - return; + return ; } } } + } + } + } + } handle: Rectangle { @@ -166,10 +200,14 @@ Window { implicitHeight: 2 color: Style.BorderColor } + } + Components.ErrorDialog { id: err + anchors.centerIn: parent width: parent.width * 0.8 } + } diff --git a/src/qml/Producer/ProtobufOptions.qml b/src/qml/Producer/ProtobufOptions.qml new file mode 100644 index 0000000..b56d552 --- /dev/null +++ b/src/qml/Producer/ProtobufOptions.qml @@ -0,0 +1,80 @@ +import QtQuick 6.0 +import QtQuick.Layouts 6.0 +import QtQuick.Controls 6.0 +import Qt.labs.platform 1.1 +import plumber 1.0 +import "../style.js" as Style + +Item { + id: itm + + property url protoFile: fileDialog.file + property alias protoMessage: messageCbx.currentText + + implicitHeight: content.height + width: 500 + + RowLayout { + id: content + + width: parent.width + spacing: 4 + + Text { + text: "Proto:" + color: Style.LabelColor + } + + Text { + id: path + + Layout.fillWidth: true + elide: Text.ElideLeft + horizontalAlignment: Text.AlignRight + text: fileDialog.file + color: Style.LabelColor + } + + Button { + text: "..." + width: 38 + implicitWidth: 38 + onClicked: fileDialog.open() + } + + ComboBox { + id: messageCbx + + width: 170 + Layout.preferredWidth: 170 + model: protoMessages + textRole: "message" + } + + } + + FileDialog { + id: fileDialog + + nameFilters: ["Proto files (*.proto)"] + onFileChanged: { + itm.protoFile = file; + } + } + + ProtobufMessagesModel { + id: protoMessages + + file: fileDialog.file + onParseErrors: (errs) => { + let e = { + "what": errs + }; + consumerErrDialog.show(e); + } + onLoaded: { + messageCbx.currentIndex = 0; + } + } + +} diff --git a/src/qml/main.qml b/src/qml/main.qml index 0afc1a2..3f263d1 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -3,6 +3,7 @@ import QtQuick.Controls 2.12 ApplicationWindow { id: appWindow + title: "Plumber" width: 700 height: 400 @@ -10,21 +11,32 @@ ApplicationWindow { StackView { id: stack + initialItem: startPage anchors.fill: parent + pushEnter: Transition { } + pushExit: Transition { } + } + Component { id: startPage + StartScreen { } + } + Component { id: mainPage + MainScreen { } + } + }