— Я духов вызывать могу из бездны!
— И я могу, и всякий это может. Вопрос лишь, явятся ль они на зов.
Шекспир, Генрих IV
Как-то так сложилось, что у нас не так много UI для Apache Kafka. А если хочется именно desktop, то Offset Explorer и упомянутый Conduktor. Первый имеет морально устаревший интерфейс 2000х, а второй не оправдано дорогой, т. к. не использую весь его богатый функционал. Вооружившись Qt и librdkafka, набросал conduktor на минималках.
Используй layout Люк

Сложный выпадающий элемент, имеющий множество состояний. Как съесть слона? По кусочкам маленького размера. Лайауты умеют отслеживать изменение видимости компонентов, и высчитывать размеры на основе implicit size объекта. Более мелки компоненты строятся следующим образом
Item {
    id: item
    implicitHeight: layout.implicitHeight
    implicitWidth: layout.implicitWidth
    property int selectedLimit: 0
    ColumnLayout {
        id: layout
        
        SpinBox {
            visible: item.selectedLimit == 1
        }
        TextField {
            visible: item.selectedLimit == 2
        }
        SpinBox {
            visible: item.selectedLimit == 3
        }
    }
}

Сворачивающая левая панель построена на манипуляции с размерами
states: [
    State {
        name: "default"
        PropertyChanges {
            target: collapsBtnLabel
            text: qsTr("« Collapse")
        }
        PropertyChanges {
            target: menu
            width: 300
            implicitWidth: 300
        }
        PropertyChanges {
            target: header
            state: "default"
        }
        PropertyChanges {
            target: kafka_icon
            source: "qrc:/kafka_icon.svg"
        }
    },
    State {
        name: "small"
        PropertyChanges {
            target: collapsBtnLabel
            text: "»"
        }
        PropertyChanges {
            target: menu
            width: 60
            implicitWidth: 60
        }
        PropertyChanges {
            target: header
            state: "small"
        }
        PropertyChanges {
            target: kafka_icon
            source: menu.broker.color !== Style.BrokerColor[0] ? "qrc:/kafka_icon_reverse.svg" : "qrc:/kafka_icon.svg"
        }
    }
]
И для сравнения как сделано в Qt Quick Controls
T.CheckDelegate {
    id: control
    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
                            implicitContentWidth + leftPadding + rightPadding)
    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
                             implicitContentHeight + topPadding + bottomPadding,
                             implicitIndicatorHeight + topPadding + bottomPadding)
    padding: 6
    spacing: 6
    icon.width: 16
    icon.height: 16
    contentItem: IconLabel {
        leftPadding: control.mirrored ? control.indicator.width + control.spacing : 0
        rightPadding: !control.mirrored ? control.indicator.width + control.spacing : 0
        spacing: control.spacing
        mirrored: control.mirrored
        display: control.display
        alignment: control.display === IconLabel.IconOnly || control.display === IconLabel.TextUnderIcon ? Qt.AlignCenter : Qt.AlignLeft
        icon: control.icon
        text: control.text
        font: control.font
        color: control.highlighted ? Fusion.highlightedText(control.palette) : control.palette.text
    }
    indicator: CheckIndicator {
        x: control.mirrored ? control.leftPadding : control.width - width - control.rightPadding
        y: control.topPadding + (control.availableHeight - height) / 2
        control: control
    }
    background: Rectangle {
        implicitWidth: 100
        implicitHeight: 20
        color: control.down ? Fusion.buttonColor(control.palette, false, true, true)
                            : control.highlighted ? Fusion.highlight(control.palette) : control.palette.base
    }
}
Twist таблицы и списка

Твист заключается в переключении между двумя режимами отображения
список
таблица
Достигается это наложением таблицы со списком, с последующей синхронизации полосы прокрутки. Делается через StackLayout,фиксации высоты делегата и двух ScrollBar
Rectangle {
    property int rowHeight: 40
    StackLayout {
        anchors.fill: parent
        
        ListView {
            Layout.fillWidth: true
            Layout.fillHeight: true
            ScrollBar.vertical: ScrollBar {
                id: listVerticalBar
                policy: ScrollBar.AsNeeded
                minimumSize: 0.06
                onPositionChanged: tableVerticalBar.position = position
            }
            
            delegate: Rectangle {
                implicitHeight: rowHeight
            }
        }
        
        TableView {
            Layout.fillWidth: true
            Layout.fillHeight: true     
            
            ScrollBar.vertical: ScrollBar {
                id: tableVerticalBar
                
                policy: ScrollBar.AsNeeded
                minimumSize: 0.06
                onPositionChanged: listVerticalBar.position = position
            }
                
            delegate: Item {
                implicitHeight: rowHeight
            }
        }
    }
}
Делегат для таблицы выглядит следующим образом
Item {
    implicitWidth: 100
    implicitHeight: rowHeight
    StackLayout {
        anchors.fill: parent
        currentIndex: column
        
        Text {
            // topic
        }
        
        Text {
            // part
        }
        //...
    }
}
Нехитрая схема, которая позволяет настраивать вид каждой колонки. Если не боитесь дополнительных зависимостей, то можете взять DelegateChooser и DelegateChoice.
Ещё раз о таблицах

Изменение количества колонок в таблице и их размер, это привычный функционал и что-то само собой разумеется. Что может пойти не так? TableView позволяет задавать функцию columnWidthProvider, которая позволяет устанавливать ширину столбца. Начиная с Qt 5.13 если вернуть 0, колонка будет скрыта.
Что бы применить изменения, дергаем forceLayout, как это сделано в документации
TableView {
    id: tableView
    property var columnWidths: [100, 50, 80, 150]
    columnWidthProvider: function (column) { return columnWidths[column] }
    
    Timer {
        running: true
        interval: 2000
        onTriggered: {
            tableView.columnWidths[2] = 150
            tableView.forceLayout();
        }
    }
}
В моём случае это выглядит так
Rectangle {
    id: main
    
    property var columnVisible: [true, true, true, true, true, true, true]
    property var columnWidths: [100, 50, 100, 150, 100, 350, 200]
    
    function columnWidthProvider(column) {
        let visible = columnVisible[column];
        let width = visible ? columnWidths[column] : 0;
        return width;
    }
    function hideColumn(column, hide) {
        columnVisible[column] = hide;
        view.forceLayout();
    }
//...
    TableView {
        id: view
        columnWidthProvider: main.columnWidthProvider
    }
}
Осталось за малым, вывести заголовок таблицы и сделать изменение размера столбца. Естественно нашелся компонент под это дело, а именно HorizontalHeaderView
HorizontalHeaderView {
    id: horizontalHeader
    reuseItems: false
    syncView: view
    height: 30
    Layout.fillWidth: true
    delegate: Rectangle {
        id: root
        implicitWidth: 50
        implicitHeight: 30
        Text {
            anchors.centerIn: parent
            text: display
            color: Style.LabelColor
            font.bold: true
        }
        Rectangle {
            id: splitter
            color: Style.BorderColor
            height: parent.height
            width: 1
            visible: mouseArea.containsMouse
            x: columnWidths[index] - 1
            onXChanged: {
                if (drag.active) {
                    main.columnWidths[index] = splitter.x + 1;
                    view.forceLayout();
                }
            }
            DragHandler {
                id: drag
                yAxis.enabled: false
                xAxis.enabled: true
                cursorShape: Qt.SizeHorCursor
            }
        }
    }
}
Да, это работает. Беремся за splitter и двигаем влево, вправо. Какие тут подводные камни? Понимающие люди обратили внимание на reuseItems: false. HorizontalHeaderView это view, а в view используется пул элементов(reusing items), что бы сэкономить на создание и удалении. Документация не рекомендует иметь делегаты с состоянием. 
На что влияет reuseItems: true в данном примере. Представьте, вы взялись за splitter и растягиваете колонку, которая имеет ширину 300. В какой-то момент view решает пере использовать элемент, возвращает в пул, достаёт от туда, и вставляет в другое место и инициализирует шириной 40. Получаем не очень понятное поведение. В моём случае такое поведение проявляется при вставке/удалении строк таблицы. 
Про окна
Интерфейс не перегружен окнами как MDI и не является SDI. Окна создаются динамически, что бы меньше имели общего состояния. Каждый такой компонент Window и создается следующим образом
function createConsumerScreen(topic, topicModel, broker) {
    let component = Qt.createComponent("qrc:/qml/Consumer/ConsumerScreen.qml");
    let posX = appWindow.x + appWindow.width/2 - Constants.ConsumerScreenWidth/2;
    let posY = appWindow.y + appWindow.height/2 - Constants.ConsumerScreenHeight/2;
    let state = {
        x: posX,
        y: posY,
        topic:topic,
        topicModel: topicModel,
        broker: broker
    };
    let consumer = component.createObject(appWindow, state);
}
function createMessageScreen(x,y,width,height,  message) {
    let component = Qt.createComponent("qrc:/qml/Consumer/MessageView.qml");
    let posX = x + width/2 - Constants.MessageViewWidth/2;
    let posY = y + height/2 - Constants.MessageViewHeight/2;
    let state = {
        x: posX,
        y: posY,
        message:message,
    };
    let consumer = component.createObject(appWindow, state);
}
Заключение
Получился прототип, который можно развивать до полноценного MVP. Из ближайших планов настроить какой-нибудь CI для сборок под Windows и Mac OS X. Весь код доступен на Git Hub.
P.S.
Передаю пламенный привет Антону Водостоеву из 2ГИС
Комментарии (7)

deanar
21.07.2021 11:30+1Скомпилировал, запустил. В поле
Additional Propertiesв конфигурации кластера не дает ввести значение. Без этого не подключиться к кластеру. macOS Big Sur 11.4.

silentiumnoxe
25.07.2021 11:46Уже есть готовое решение один в один, Kowl называется 

RPG18 Автор
25.07.2021 11:56А так же есть https://github.com/provectus/kafka-ui. Из того что посмотрел это как правило Java/Go приложение с web интерфейсом. Я хочу ещё уметь использовать существующий Go код что бы парсить и преобразовывать сообщения. Ну и штуки типа XML -> JSON, а потом прогнать через jq. JQ вроде бы можно подцепить как сишную либу.
          
 
ivymike
А зачем это всё?
RPG18 Автор
грепать топики в проде