From e0bcd2083f324cb69a459b5c855abd9f6f6b8827 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Wed, 1 Jul 2026 23:35:09 +0500 Subject: [PATCH 01/14] Compact main flows workspace and remove redundant headings --- experimental/tauri-ui-spike/web/index.html | 14 ++-- experimental/tauri-ui-spike/web/main.js | 5 -- experimental/tauri-ui-spike/web/styles.css | 73 ++++++++++++--------- src/ui/qml/components/FlowTable.qml | 26 ++------ src/ui/qml/components/FlowWorkspacePane.qml | 6 +- src/ui/qml/components/PacketDetailsPane.qml | 58 ++++++++-------- src/ui/qml/components/PacketList.qml | 26 ++------ src/ui/qml/components/StreamView.qml | 36 ++++------ 8 files changed, 107 insertions(+), 137 deletions(-) diff --git a/experimental/tauri-ui-spike/web/index.html b/experimental/tauri-ui-spike/web/index.html index e24a772..35a2a09 100644 --- a/experimental/tauri-ui-spike/web/index.html +++ b/experimental/tauri-ui-spike/web/index.html @@ -109,10 +109,9 @@
-
-
-

Flows

-

No capture loaded.

+
+
+

No capture loaded.

@@ -174,10 +173,9 @@

Flows

-
-
-

Selected-Flow Packets

-

Select a flow to load packets.

+
+
+

Select a flow to load packets.

diff --git a/experimental/tauri-ui-spike/web/main.js b/experimental/tauri-ui-spike/web/main.js index f0c85a9..0f45541 100644 --- a/experimental/tauri-ui-spike/web/main.js +++ b/experimental/tauri-ui-spike/web/main.js @@ -236,7 +236,6 @@ packetMarkerHeader: document.getElementById("packetMarkerHeader"), packetLoadMoreButton: document.getElementById("packetLoadMoreButton"), streamLoadMoreButton: document.getElementById("streamLoadMoreButton"), - flowViewTitle: document.getElementById("flowViewTitle"), streamTableBody: document.getElementById("streamTableBody"), packetDetailsTitle: document.getElementById("packetDetailsTitle"), packetDetailsMeta: document.getElementById("packetDetailsMeta"), @@ -1996,10 +1995,6 @@ panel.classList.toggle("active", panel.dataset.flowViewPanel === state.flowViewTab); } - elements.flowViewTitle.textContent = state.flowViewTab === "stream" - ? "Stream" - : (state.unrecognizedPacketsSelected ? "Unrecognized Packets" : "Selected-Flow Packets"); - const showingPackets = state.flowViewTab === "packets"; if (elements.flowViewTabStreamButton) { elements.flowViewTabStreamButton.disabled = state.unrecognizedPacketsSelected; diff --git a/experimental/tauri-ui-spike/web/styles.css b/experimental/tauri-ui-spike/web/styles.css index a04fdb1..4abcee4 100644 --- a/experimental/tauri-ui-spike/web/styles.css +++ b/experimental/tauri-ui-spike/web/styles.css @@ -615,7 +615,7 @@ body.is-resizing-vertical { .workspace-shell { display: grid; grid-template-rows: auto 1fr; - gap: 8px; + gap: 6px; min-height: 0; } @@ -763,7 +763,7 @@ body.is-resizing-vertical { .details-scroll { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; padding-right: 2px; } @@ -772,12 +772,12 @@ body.is-resizing-vertical { align-items: flex-start; justify-content: space-between; gap: 10px; - margin-bottom: 6px; + margin-bottom: 4px; flex: 0 0 auto; } .flows-panel .panel-heading { - margin-bottom: 4px; + margin-bottom: 2px; } .panel-heading h2 { @@ -785,6 +785,19 @@ body.is-resizing-vertical { font-size: 16px; } +.panel-heading-compact { + align-items: center; + min-height: 0; +} + +.panel-heading-compact .panel-heading-copy { + min-width: 0; +} + +.panel-subtitle-inline { + margin: 0; +} + .panel-subtitle { margin: 1px 0 0; color: var(--muted); @@ -817,7 +830,7 @@ body.is-resizing-vertical { .flow-wireshark-row { display: grid; grid-template-columns: 110px minmax(0, 1fr) auto; - gap: 6px; + gap: 5px; align-items: center; } @@ -966,7 +979,7 @@ select:disabled { display: flex; align-items: flex-end; gap: 4px; - margin-bottom: 6px; + margin-bottom: 4px; flex: 0 0 auto; padding: 0 2px; border-bottom: 1px solid var(--border); @@ -974,8 +987,8 @@ select:disabled { .subtab-button { min-width: 96px; - min-height: 34px; - padding: 7px 14px 8px; + min-height: 30px; + padding: 6px 14px 7px; border: 1px solid var(--border); border-bottom: none; border-radius: 10px 10px 0 0; @@ -1006,7 +1019,7 @@ select:disabled { display: flex; align-items: flex-end; gap: 4px; - margin-bottom: 6px; + margin-bottom: 4px; flex: 0 0 auto; flex-wrap: wrap; padding: 0 2px; @@ -1015,8 +1028,8 @@ select:disabled { .inspector-tab { min-width: 96px; - min-height: 30px; - padding: 6px 12px 7px; + min-height: 28px; + padding: 5px 12px 6px; border: 1px solid var(--border); border-bottom: none; border-radius: 8px 8px 0 0; @@ -1073,7 +1086,7 @@ select:disabled { .data-table th, .data-table td { - padding: 6px 8px; + padding: 5px 7px; border-bottom: 1px solid #e3eaf2; text-align: left; vertical-align: top; @@ -1277,8 +1290,8 @@ select:disabled { justify-content: space-between; gap: 10px; width: 100%; - margin-top: 8px; - padding: 9px 10px; + margin-top: 6px; + padding: 8px 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface-muted); @@ -1858,7 +1871,7 @@ select:disabled { display: flex; flex: 1 1 auto; flex-direction: column; - gap: 6px; + gap: 4px; min-height: 0; } @@ -1871,7 +1884,7 @@ select:disabled { display: flex; flex: 1 1 auto; flex-direction: column; - gap: 6px; + gap: 4px; min-height: 0; } @@ -1888,7 +1901,7 @@ select:disabled { .packet-summary-layers { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } .packet-summary-layer { @@ -1907,11 +1920,11 @@ select:disabled { align-items: center; justify-content: space-between; gap: 8px; - padding: 8px 10px; + padding: 6px 9px; cursor: pointer; list-style: none; color: var(--text-primary); - font-size: 13px; + font-size: 12px; font-weight: 400; } @@ -1957,20 +1970,20 @@ select:disabled { .packet-summary-layer-body { display: flex; flex-direction: column; - gap: 8px; - padding: 0 10px 10px; + gap: 6px; + padding: 0 9px 8px; } .packet-summary-fields { display: flex; flex-direction: column; - gap: 5px; + gap: 4px; } .packet-summary-field { display: grid; grid-template-columns: minmax(128px, auto) 1fr; - gap: 6px 10px; + gap: 4px 8px; align-items: start; } @@ -1994,8 +2007,8 @@ select:disabled { .packet-summary-children { display: flex; flex-direction: column; - gap: 8px; - padding-top: 8px; + gap: 6px; + padding-top: 6px; border-top: 1px solid #e2e8f0; } @@ -2044,7 +2057,7 @@ select:disabled { .details-pre { margin: 0; - padding: 7px 9px; + padding: 6px 8px; border: 1px solid #dfe7f1; border-radius: 8px; background: var(--surface-bg); @@ -2067,7 +2080,7 @@ select:disabled { } .stream-details-header-card { - padding: 9px 10px; + padding: 8px 9px; border: 1px solid #dbe4ee; border-radius: 10px; background: var(--surface-bg); @@ -2140,9 +2153,9 @@ select:disabled { .stream-card-list { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; min-height: 100%; - padding: 8px; + padding: 6px; } .stream-list-state { @@ -2175,7 +2188,7 @@ select:disabled { .stream-card { width: min(84%, 420px); - padding: 9px 10px; + padding: 8px 9px; border: 1px solid #c9e1d1; border-radius: 10px; background: #f2faf4; diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index 792aa3d..0687933 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -14,9 +14,9 @@ Frame { property int unrecognizedPacketCount: 0 property int sortColumn: 0 property bool sortAscending: true - readonly property int tableRowSpacing: 8 - readonly property int tableContentLeftMargin: 8 - readonly property int tableContentRightMargin: 8 + readonly property int tableRowSpacing: 6 + readonly property int tableContentLeftMargin: 6 + readonly property int tableContentRightMargin: 6 readonly property int selectionColumnWidth: 42 readonly property int indexColumnWidth: 64 readonly property int familyColumnWidth: 74 @@ -96,13 +96,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 - - Label { - text: "Flows" - font.pixelSize: 18 - font.bold: true - } + spacing: 6 RowLayout { Layout.fillWidth: true @@ -165,12 +159,6 @@ Frame { } } - Rectangle { - Layout.fillWidth: true - height: 1 - color: "#e2e8f0" - } - RowLayout { Layout.fillWidth: true Layout.leftMargin: root.tableContentLeftMargin @@ -243,7 +231,7 @@ Frame { } width: flowListView.width - height: 36 + height: 32 color: selected ? "#dbeafe" : (index % 2 === 0 ? "#ffffff" : "#f8fafc") @@ -392,7 +380,7 @@ Frame { } Rectangle { Layout.preferredWidth: root.fragColumnWidth - implicitHeight: 24 + implicitHeight: 20 radius: 4 color: root.fragBackgroundColor(hasFragmentedPackets, selected) border.width: color === "transparent" ? 0 : 1 @@ -451,7 +439,7 @@ Frame { border.color: root.unrecognizedPacketsSelected ? "#93c5fd" : "#d8dee9" border.width: 1 radius: 6 - implicitHeight: 46 + implicitHeight: 40 RowLayout { anchors.fill: parent diff --git a/src/ui/qml/components/FlowWorkspacePane.qml b/src/ui/qml/components/FlowWorkspacePane.qml index 1ee6cec..3e0daab 100644 --- a/src/ui/qml/components/FlowWorkspacePane.qml +++ b/src/ui/qml/components/FlowWorkspacePane.qml @@ -103,7 +103,7 @@ Item { ColumnLayout { anchors.fill: parent - spacing: 8 + spacing: 6 TabBar { id: flowDetailTabs @@ -124,7 +124,7 @@ Item { TabButton { text: "Packets" - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text @@ -148,7 +148,7 @@ Item { TabButton { text: "Stream" - implicitHeight: 34 + implicitHeight: 30 enabled: !root.unrecognizedPacketsSelected contentItem: Label { diff --git a/src/ui/qml/components/PacketDetailsPane.qml b/src/ui/qml/components/PacketDetailsPane.qml index e75fd84..024969b 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -447,8 +447,8 @@ Frame { id: rowLayout anchors.fill: parent columns: fullWidth ? 1 : 2 - columnSpacing: 8 - rowSpacing: 2 + columnSpacing: 6 + rowSpacing: 1 Label { visible: !fullWidth @@ -502,13 +502,13 @@ Frame { color: "#fbfcfe" border.color: warningState ? "#f4c97d" : "#dbe4ee" radius: 8 - implicitHeight: layerColumn.implicitHeight + 16 + implicitHeight: layerColumn.implicitHeight + 12 ColumnLayout { id: layerColumn anchors.fill: parent - anchors.margins: 8 - spacing: 6 + anchors.margins: 6 + spacing: 4 RowLayout { Layout.fillWidth: true @@ -527,8 +527,8 @@ Frame { ) } padding: 0 - implicitWidth: 18 - implicitHeight: 18 + implicitWidth: 16 + implicitHeight: 16 contentItem: Label { text: parent.text @@ -546,7 +546,7 @@ Frame { Label { Layout.fillWidth: true text: summaryLayerCard.titleText - font.pixelSize: 13 + font.pixelSize: 12 font.bold: false color: "#0f172a" } @@ -573,7 +573,7 @@ Frame { ColumnLayout { Layout.fillWidth: true visible: summaryLayerCard.expanded - spacing: 5 + spacing: 4 Repeater { model: summaryLayerCard.fieldRows @@ -656,7 +656,7 @@ Frame { Label { Layout.fillWidth: true text: root.headerPrimaryText() - font.pixelSize: 15 + font.pixelSize: 14 font.bold: true color: "#0f172a" elide: Text.ElideRight @@ -709,7 +709,7 @@ Frame { text: parent.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 font.bold: parent.checked color: parent.checked ? "#0f172a" : "#64748b" } @@ -727,13 +727,13 @@ Frame { TabButton { text: "Raw" - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 font.bold: parent.checked color: parent.checked ? "#0f172a" : "#64748b" } @@ -751,13 +751,13 @@ Frame { TabButton { text: root.payloadTabTitle() - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 font.bold: parent.checked color: parent.checked ? "#0f172a" : "#64748b" } @@ -775,13 +775,13 @@ Frame { TabButton { text: "Protocol" - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 font.bold: parent.checked color: parent.checked ? "#0f172a" : "#64748b" } @@ -810,13 +810,13 @@ Frame { TabButton { text: "Summary" - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 font.bold: parent.checked color: parent.checked ? "#0f172a" : "#64748b" } @@ -834,13 +834,13 @@ Frame { TabButton { text: root.payloadTabTitle() - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 font.bold: parent.checked color: parent.checked ? "#0f172a" : "#64748b" } @@ -858,13 +858,13 @@ Frame { TabButton { text: "Protocol" - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 font.bold: parent.checked color: parent.checked ? "#0f172a" : "#64748b" } @@ -899,7 +899,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 + spacing: 6 Rectangle { Layout.fillWidth: true @@ -907,14 +907,14 @@ Frame { color: "#fff6d6" border.color: "#e7d38d" radius: 6 - implicitHeight: warningLabel.implicitHeight + 16 + implicitHeight: warningLabel.implicitHeight + 12 Text { id: warningLabel anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: 7 + anchors.margins: 6 wrapMode: Text.Wrap color: "#7a5d10" text: packetSummaryPane.warningText.length > 0 @@ -933,7 +933,7 @@ Frame { ColumnLayout { width: parent.width - spacing: 8 + spacing: 6 Repeater { model: packetSummaryPane.layers @@ -1006,14 +1006,14 @@ Frame { color: "#fff6d6" border.color: "#e7d38d" radius: 6 - implicitHeight: streamWarningLabel.implicitHeight + 16 + implicitHeight: streamWarningLabel.implicitHeight + 12 Text { id: streamWarningLabel anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: 7 + anchors.margins: 6 wrapMode: Text.Wrap color: "#7a5d10" text: parent.parent.parent.warningText.length > 0 diff --git a/src/ui/qml/components/PacketList.qml b/src/ui/qml/components/PacketList.qml index 7bdb612..7e86f6d 100644 --- a/src/ui/qml/components/PacketList.qml +++ b/src/ui/qml/components/PacketList.qml @@ -164,19 +164,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 - - Label { - text: root.titleText - font.pixelSize: 18 - font.bold: true - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: "#e2e8f0" - } + spacing: 6 RowLayout { Layout.fillWidth: true @@ -203,7 +191,7 @@ Frame { RowLayout { Layout.fillWidth: true - spacing: 10 + spacing: 8 Label { text: "#" @@ -293,7 +281,7 @@ Frame { readonly property bool selected: index === packetListView.currentIndex width: packetListView.width - height: 34 + height: 30 color: root.rowBackgroundColor(index, capturedLength, originalLength, selected) RowLayout { @@ -311,7 +299,7 @@ Frame { Rectangle { Layout.preferredWidth: 68 - implicitHeight: 24 + implicitHeight: 20 radius: 4 color: root.directionBackgroundColor(directionText, selected) border.width: color === "transparent" ? 0 : 1 @@ -338,7 +326,7 @@ Frame { Rectangle { Layout.preferredWidth: 72 - implicitHeight: 24 + implicitHeight: 20 radius: 4 color: root.capturedBackgroundColor(isIpFragmented, selected) border.width: color === "transparent" ? 0 : 1 @@ -363,7 +351,7 @@ Frame { Rectangle { Layout.fillWidth: true - implicitHeight: 24 + implicitHeight: 20 radius: 4 color: root.unrecognizedMode ? "transparent" @@ -398,7 +386,7 @@ Frame { Rectangle { Layout.preferredWidth: 168 - implicitHeight: 24 + implicitHeight: 20 visible: root.showMarkerColumn radius: 4 color: suspectedTcpRetransmission && !selected ? "#fff1cc" : "transparent" diff --git a/src/ui/qml/components/StreamView.qml b/src/ui/qml/components/StreamView.qml index 7cbc8d6..df8891f 100644 --- a/src/ui/qml/components/StreamView.qml +++ b/src/ui/qml/components/StreamView.qml @@ -67,19 +67,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 - - Label { - text: "Stream" - font.pixelSize: 18 - font.bold: true - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: "#e2e8f0" - } + spacing: 6 RowLayout { Layout.fillWidth: true @@ -143,9 +131,9 @@ Frame { ListView { id: streamListView anchors.fill: parent - anchors.margins: 8 + anchors.margins: 6 clip: true - spacing: 8 + spacing: 6 model: root.streamModel ScrollBar.vertical: ScrollBar { @@ -174,8 +162,8 @@ Frame { id: bubble x: forward ? 0 : parent.width - width width: Math.min(streamListView.width * 0.84, 420) - implicitHeight: metadataContainer.y + metadataContainer.implicitHeight + 10 - radius: 10 + implicitHeight: metadataContainer.y + metadataContainer.implicitHeight + 8 + radius: 9 color: root.bubbleColor(directionText, selected) border.color: root.bubbleBorderColor(directionText, selected) border.width: selected ? 2 : 1 @@ -185,8 +173,8 @@ Frame { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: 9 - spacing: 8 + anchors.margins: 8 + spacing: 7 Item { Layout.fillWidth: true @@ -197,7 +185,7 @@ Frame { anchors.fill: parent text: label font.bold: true - font.pixelSize: 13 + font.pixelSize: 12 color: "#0f172a" elide: Text.ElideRight verticalAlignment: Text.AlignVCenter @@ -227,9 +215,9 @@ Frame { anchors.left: parent.left anchors.right: parent.right anchors.top: topRow.bottom - anchors.leftMargin: 9 - anchors.rightMargin: 9 - anchors.topMargin: 4 + anchors.leftMargin: 8 + anchors.rightMargin: 8 + anchors.topMargin: 3 implicitHeight: Math.max(metadataTextItem.implicitHeight, constrictedBadge.visible ? constrictedBadge.implicitHeight : 0) Label { @@ -242,7 +230,7 @@ Frame { color: "#475569" elide: Text.ElideRight verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 } MouseArea { From 4c5f1181c4e137af4afc30dbfe10e62ee4c17ef7 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Wed, 1 Jul 2026 23:53:10 +0500 Subject: [PATCH 02/14] Compact flow endpoints into two columns --- experimental/tauri-ui-spike/web/main.js | 28 +++++++-- experimental/tauri-ui-spike/web/styles.css | 5 ++ src/ui/qml/components/FlowTable.qml | 73 ++++++++++++---------- 3 files changed, 68 insertions(+), 38 deletions(-) diff --git a/experimental/tauri-ui-spike/web/main.js b/experimental/tauri-ui-spike/web/main.js index 0f45541..0e80a1e 100644 --- a/experimental/tauri-ui-spike/web/main.js +++ b/experimental/tauri-ui-spike/web/main.js @@ -576,6 +576,24 @@ return state.packets.some((packet) => packetMarkerText(packet).length > 0); } + function formatEndpoint(address, port) { + const trimmedAddress = String(address || "").trim(); + const numericPort = Number(port); + const hasPort = Number.isFinite(numericPort) && numericPort > 0; + + if (!trimmedAddress) { + return ""; + } + + const displayAddress = trimmedAddress.includes(":") + ? `[${trimmedAddress}]` + : trimmedAddress; + + return hasPort + ? `${displayAddress} : ${numericPort}` + : displayAddress; + } + function unrecognizedPacketCount() { return Number(state.overview?.unrecognized_packet_count || 0); } @@ -1125,9 +1143,9 @@ case "frag": return Number(flow?.fragmented_packet_count ?? 0); case "endpoint_a": - return String(flow?.endpoint_a || `${flow?.address_a || ""}:${flow?.port_a ?? ""}`); + return formatEndpoint(flow?.address_a, flow?.port_a) || String(flow?.endpoint_a || ""); case "endpoint_b": - return String(flow?.endpoint_b || `${flow?.address_b || ""}:${flow?.port_b ?? ""}`); + return formatEndpoint(flow?.address_b, flow?.port_b) || String(flow?.endpoint_b || ""); case "packets": return Number(flow?.packet_count ?? 0); case "bytes": @@ -2415,8 +2433,8 @@ ${escapeHtml(formatProtocolHint(flow))} ${escapeHtml(flow.service_hint)} ${escapeHtml(formatFlowFragmentMarker(flow))} - ${escapeHtml(flow.address_a)}:${flow.port_a} - ${escapeHtml(flow.address_b)}:${flow.port_b} + ${escapeHtml(formatEndpoint(flow.address_a, flow.port_a))} + ${escapeHtml(formatEndpoint(flow.address_b, flow.port_b))} ${formatPlainInteger(flow.packet_count)} ${formatPlainInteger(flow.total_bytes)} @@ -3571,7 +3589,7 @@ renderRow: (flow) => { const selected = state.selectedFlowIndex === flow.flow_index ? " selected" : ""; const hintOrProtocol = formatProtocolHint(flow) || flow.protocol_text || "-"; - const endpointSummary = `${flow.address_a}:${flow.port_a} <-> ${flow.address_b}:${flow.port_b}`; + const endpointSummary = `${formatEndpoint(flow.address_a, flow.port_a)} <-> ${formatEndpoint(flow.address_b, flow.port_b)}`; const titleText = flow.service_hint ? `${flow.service_hint}\n${endpointSummary}` : endpointSummary; diff --git a/experimental/tauri-ui-spike/web/styles.css b/experimental/tauri-ui-spike/web/styles.css index 4abcee4..60a7f33 100644 --- a/experimental/tauri-ui-spike/web/styles.css +++ b/experimental/tauri-ui-spike/web/styles.css @@ -1142,6 +1142,11 @@ select:disabled { background: var(--selected); } +.flow-endpoint-cell { + font-family: "Cascadia Mono", Consolas, monospace; + white-space: nowrap; +} + .data-table tbody tr.packet-row.is-warning { background: #fff8e7; } diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index 0687933..31095e9 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -22,9 +22,8 @@ Frame { readonly property int familyColumnWidth: 74 readonly property int protocolColumnWidth: 86 readonly property int protocolHintColumnWidth: 98 - readonly property int serviceColumnWidth: 220 - readonly property int addressColumnWidth: 180 - readonly property int portColumnWidth: 78 + readonly property int serviceColumnWidth: 200 + readonly property int endpointColumnWidth: 220 readonly property int fragColumnWidth: 56 readonly property int packetsColumnWidth: 86 readonly property int bytesColumnWidth: 92 @@ -80,6 +79,24 @@ Frame { return hasFragmentedPackets ? "#8a6a12" : "#0f172a" } + function formatEndpoint(address, port) { + const trimmedAddress = address ? String(address).trim() : "" + const numericPort = Number(port) + const hasPort = Number.isFinite(numericPort) && numericPort > 0 + + if (trimmedAddress.length === 0) { + return "" + } + + const displayAddress = trimmedAddress.indexOf(":") >= 0 + ? "[" + trimmedAddress + "]" + : trimmedAddress + + return hasPort + ? displayAddress + " : " + numericPort + : displayAddress + } + background: Rectangle { color: "#ffffff" border.color: "#d8dee9" @@ -171,10 +188,8 @@ Frame { Button { text: "Protocol" + root.sortIndicator(2); Layout.preferredWidth: root.protocolColumnWidth; onClicked: root.sortRequested(2) } Button { text: "Proto Hint" + root.sortIndicator(3); Layout.preferredWidth: root.protocolHintColumnWidth; onClicked: root.sortRequested(3) } Button { text: "Service" + root.sortIndicator(4); Layout.fillWidth: true; Layout.preferredWidth: root.serviceColumnWidth; onClicked: root.sortRequested(4) } - Button { text: "Address A" + root.sortIndicator(6); Layout.fillWidth: true; Layout.preferredWidth: root.addressColumnWidth; onClicked: root.sortRequested(6) } - Button { text: "Port A" + root.sortIndicator(7); Layout.preferredWidth: root.portColumnWidth; onClicked: root.sortRequested(7) } - Button { text: "Address B" + root.sortIndicator(8); Layout.fillWidth: true; Layout.preferredWidth: root.addressColumnWidth; onClicked: root.sortRequested(8) } - Button { text: "Port B" + root.sortIndicator(9); Layout.preferredWidth: root.portColumnWidth; onClicked: root.sortRequested(9) } + Button { text: "Endpoint A" + root.sortIndicator(6); Layout.fillWidth: true; Layout.preferredWidth: root.endpointColumnWidth; onClicked: root.sortRequested(6) } + Button { text: "Endpoint B" + root.sortIndicator(8); Layout.fillWidth: true; Layout.preferredWidth: root.endpointColumnWidth; onClicked: root.sortRequested(8) } Button { text: "Frag" + root.sortIndicator(5); Layout.preferredWidth: root.fragColumnWidth; onClicked: root.sortRequested(5) } Button { text: "Packets" + root.sortIndicator(10); Layout.preferredWidth: root.packetsColumnWidth; onClicked: root.sortRequested(10) } Button { text: "Bytes" + root.sortIndicator(11); Layout.preferredWidth: root.bytesColumnWidth; onClicked: root.sortRequested(11) } @@ -223,6 +238,8 @@ Frame { required property string bytes readonly property bool selected: index === flowListView.currentIndex + readonly property string endpointAText: root.formatEndpoint(addressA, portA) + readonly property string endpointBText: root.formatEndpoint(addressB, portB) onFlowCheckedChanged: { if (selectionCheckBox.checked !== flowChecked) { @@ -322,61 +339,51 @@ Frame { Item { Layout.fillWidth: true - Layout.preferredWidth: root.addressColumnWidth - implicitHeight: addressALabel.implicitHeight + Layout.preferredWidth: root.endpointColumnWidth + implicitHeight: endpointALabel.implicitHeight Label { - id: addressALabel + id: endpointALabel anchors.fill: parent - text: addressA + text: endpointAText + font.family: "Consolas" elide: Text.ElideMiddle verticalAlignment: Text.AlignVCenter } MouseArea { - id: addressAHoverArea + id: endpointAHoverArea anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true } - ToolTip.visible: addressAHoverArea.containsMouse && addressALabel.truncated - ToolTip.text: addressALabel.text - } - Text { - text: portA - Layout.preferredWidth: root.portColumnWidth - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter + ToolTip.visible: endpointAHoverArea.containsMouse && endpointALabel.truncated + ToolTip.text: endpointALabel.text } Item { Layout.fillWidth: true - Layout.preferredWidth: root.addressColumnWidth - implicitHeight: addressBLabel.implicitHeight + Layout.preferredWidth: root.endpointColumnWidth + implicitHeight: endpointBLabel.implicitHeight Label { - id: addressBLabel + id: endpointBLabel anchors.fill: parent - text: addressB + text: endpointBText + font.family: "Consolas" elide: Text.ElideMiddle verticalAlignment: Text.AlignVCenter } MouseArea { - id: addressBHoverArea + id: endpointBHoverArea anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true } - ToolTip.visible: addressBHoverArea.containsMouse && addressBLabel.truncated - ToolTip.text: addressBLabel.text - } - Text { - text: portB - Layout.preferredWidth: root.portColumnWidth - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter + ToolTip.visible: endpointBHoverArea.containsMouse && endpointBLabel.truncated + ToolTip.text: endpointBLabel.text } Rectangle { Layout.preferredWidth: root.fragColumnWidth From 7f8a8647bd245cfa5eb4a9eb72ea7f16726a8129 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 00:12:30 +0500 Subject: [PATCH 03/14] Refine flow endpoint columns and compact Tauri rows --- experimental/tauri-ui-spike/web/main.js | 41 +++++++- experimental/tauri-ui-spike/web/styles.css | 35 ++++++- src/ui/qml/components/FlowTable.qml | 111 +++++++++++++++++---- 3 files changed, 165 insertions(+), 22 deletions(-) diff --git a/experimental/tauri-ui-spike/web/main.js b/experimental/tauri-ui-spike/web/main.js index 0e80a1e..f649585 100644 --- a/experimental/tauri-ui-spike/web/main.js +++ b/experimental/tauri-ui-spike/web/main.js @@ -12,7 +12,7 @@ const streamItemBatchSize = 15; const initialStreamPacketBudget = 30; const streamPacketBatchSize = 30; - const flowVirtualRowHeight = 36; + const flowVirtualRowHeight = 32; const analysisFlowVirtualRowHeight = 44; const flowVirtualOverscanRows = 12; const analysisFlowVirtualOverscanRows = 10; @@ -594,6 +594,41 @@ : displayAddress; } + function formatEndpointParts(address, port) { + const trimmedAddress = String(address || "").trim(); + const numericPort = Number(port); + const hasPort = Number.isFinite(numericPort) && numericPort > 0; + + if (!trimmedAddress) { + return { + address: "", + hasPort: false, + port: "", + }; + } + + const displayAddress = hasPort && trimmedAddress.includes(":") + ? `[${trimmedAddress}]` + : trimmedAddress; + + return { + address: displayAddress, + hasPort, + port: hasPort ? String(numericPort) : "", + }; + } + + function renderEndpointCell(address, port) { + const parts = formatEndpointParts(address, port); + return ` + + ${escapeHtml(parts.address)} + : + ${escapeHtml(parts.port)} + + `; + } + function unrecognizedPacketCount() { return Number(state.overview?.unrecognized_packet_count || 0); } @@ -2433,8 +2468,8 @@ ${escapeHtml(formatProtocolHint(flow))} ${escapeHtml(flow.service_hint)} ${escapeHtml(formatFlowFragmentMarker(flow))} - ${escapeHtml(formatEndpoint(flow.address_a, flow.port_a))} - ${escapeHtml(formatEndpoint(flow.address_b, flow.port_b))} + ${renderEndpointCell(flow.address_a, flow.port_a)} + ${renderEndpointCell(flow.address_b, flow.port_b)} ${formatPlainInteger(flow.packet_count)} ${formatPlainInteger(flow.total_bytes)} diff --git a/experimental/tauri-ui-spike/web/styles.css b/experimental/tauri-ui-spike/web/styles.css index 60a7f33..ecf99f2 100644 --- a/experimental/tauri-ui-spike/web/styles.css +++ b/experimental/tauri-ui-spike/web/styles.css @@ -1142,11 +1142,44 @@ select:disabled { background: var(--selected); } +.flow-table-wrap .data-table th, +.flow-table-wrap .data-table td { + padding-top: 4px; + padding-bottom: 4px; + line-height: 1.15; +} + .flow-endpoint-cell { font-family: "Cascadia Mono", Consolas, monospace; white-space: nowrap; } +.endpoint-cell-inner { + display: inline-grid; + grid-template-columns: minmax(0, 16ch) 1ch 5ch; + align-items: center; + column-gap: 0.35rem; + justify-content: start; + max-width: 100%; + min-width: 0; +} + +.endpoint-address { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.endpoint-separator { + text-align: center; + color: var(--text-muted); +} + +.endpoint-port { + text-align: right; +} + .data-table tbody tr.packet-row.is-warning { background: #fff8e7; } @@ -1247,7 +1280,7 @@ select:disabled { } .flow-table-wrap .data-table tbody tr.flow-row td { - height: 36px; + height: 32px; vertical-align: middle; white-space: nowrap; } diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index 31095e9..cad67ba 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -24,6 +24,9 @@ Frame { readonly property int protocolHintColumnWidth: 98 readonly property int serviceColumnWidth: 200 readonly property int endpointColumnWidth: 220 + readonly property int endpointAddressSlotWidth: 136 + readonly property int endpointPortWidth: 44 + readonly property int endpointSeparatorWidth: 10 readonly property int fragColumnWidth: 56 readonly property int packetsColumnWidth: 86 readonly property int bytesColumnWidth: 92 @@ -97,6 +100,24 @@ Frame { : displayAddress } + function endpointHasPort(port) { + const numericPort = Number(port) + return Number.isFinite(numericPort) && numericPort > 0 + } + + function endpointDisplayAddress(address, port) { + const trimmedAddress = address ? String(address).trim() : "" + if (trimmedAddress.length === 0) { + return "" + } + + if (root.endpointHasPort(port) && trimmedAddress.indexOf(":") >= 0) { + return "[" + trimmedAddress + "]" + } + + return trimmedAddress + } + background: Rectangle { color: "#ffffff" border.color: "#d8dee9" @@ -238,6 +259,10 @@ Frame { required property string bytes readonly property bool selected: index === flowListView.currentIndex + readonly property bool endpointAHasPort: root.endpointHasPort(portA) + readonly property bool endpointBHasPort: root.endpointHasPort(portB) + readonly property string endpointAAddressText: root.endpointDisplayAddress(addressA, portA) + readonly property string endpointBAddressText: root.endpointDisplayAddress(addressB, portB) readonly property string endpointAText: root.formatEndpoint(addressA, portA) readonly property string endpointBText: root.formatEndpoint(addressB, portB) @@ -340,15 +365,40 @@ Frame { Item { Layout.fillWidth: true Layout.preferredWidth: root.endpointColumnWidth - implicitHeight: endpointALabel.implicitHeight + implicitHeight: endpointARow.implicitHeight + + RowLayout { + id: endpointARow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + Label { + id: endpointALabel + Layout.preferredWidth: root.endpointAddressSlotWidth + text: endpointAAddressText + font.family: "Consolas" + elide: Text.ElideMiddle + verticalAlignment: Text.AlignVCenter + } - Label { - id: endpointALabel - anchors.fill: parent - text: endpointAText - font.family: "Consolas" - elide: Text.ElideMiddle - verticalAlignment: Text.AlignVCenter + Label { + visible: endpointAHasPort + Layout.preferredWidth: root.endpointSeparatorWidth + horizontalAlignment: Text.AlignHCenter + text: ":" + font.family: "Consolas" + color: "#64748b" + } + + Label { + visible: endpointAHasPort + Layout.preferredWidth: root.endpointPortWidth + horizontalAlignment: Text.AlignRight + text: portA + font.family: "Consolas" + color: "#0f172a" + } } MouseArea { @@ -359,20 +409,45 @@ Frame { } ToolTip.visible: endpointAHoverArea.containsMouse && endpointALabel.truncated - ToolTip.text: endpointALabel.text + ToolTip.text: endpointAText } Item { Layout.fillWidth: true Layout.preferredWidth: root.endpointColumnWidth - implicitHeight: endpointBLabel.implicitHeight + implicitHeight: endpointBRow.implicitHeight + + RowLayout { + id: endpointBRow + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + Label { + id: endpointBLabel + Layout.preferredWidth: root.endpointAddressSlotWidth + text: endpointBAddressText + font.family: "Consolas" + elide: Text.ElideMiddle + verticalAlignment: Text.AlignVCenter + } - Label { - id: endpointBLabel - anchors.fill: parent - text: endpointBText - font.family: "Consolas" - elide: Text.ElideMiddle - verticalAlignment: Text.AlignVCenter + Label { + visible: endpointBHasPort + Layout.preferredWidth: root.endpointSeparatorWidth + horizontalAlignment: Text.AlignHCenter + text: ":" + font.family: "Consolas" + color: "#64748b" + } + + Label { + visible: endpointBHasPort + Layout.preferredWidth: root.endpointPortWidth + horizontalAlignment: Text.AlignRight + text: portB + font.family: "Consolas" + color: "#0f172a" + } } MouseArea { @@ -383,7 +458,7 @@ Frame { } ToolTip.visible: endpointBHoverArea.containsMouse && endpointBLabel.truncated - ToolTip.text: endpointBLabel.text + ToolTip.text: endpointBText } Rectangle { Layout.preferredWidth: root.fragColumnWidth From 4258963088f1948f9074feffb3602272c18821cc Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 01:02:06 +0500 Subject: [PATCH 04/14] Use lightweight selectable text in Qt packet summary --- src/ui/app/main.cpp | 2 +- src/ui/qml/components/PacketDetailsPane.qml | 49 +++++++++++++-------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/ui/app/main.cpp b/src/ui/app/main.cpp index 87c6422..0976677 100644 --- a/src/ui/app/main.cpp +++ b/src/ui/app/main.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/ui/qml/components/PacketDetailsPane.qml b/src/ui/qml/components/PacketDetailsPane.qml index 024969b..37087b7 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -431,6 +431,23 @@ Frame { } } + component SelectableText: TextEdit { + property color textColor: "#0f172a" + property bool monospace: false + property bool bold: false + property int textWrapMode: TextEdit.NoWrap + + readOnly: true + selectByMouse: true + textFormat: TextEdit.PlainText + wrapMode: textWrapMode + color: textColor + font.family: monospace ? "Consolas" : "" + font.pixelSize: 12 + font.bold: bold + cursorVisible: activeFocus + } + component SummaryFieldRow: Item { required property var modelData readonly property string labelText: modelData && modelData["label"] !== undefined && modelData["label"] !== null @@ -450,20 +467,17 @@ Frame { columnSpacing: 6 rowSpacing: 1 - Label { + SelectableText { visible: !fullWidth text: fullWidth ? "" : labelText - color: "#64748b" - font.pixelSize: 12 + textColor: "#64748b" } - Label { + SelectableText { Layout.fillWidth: true text: valueText - color: "#0f172a" - font.pixelSize: 12 - font.bold: false - wrapMode: Text.Wrap + textColor: "#0f172a" + textWrapMode: TextEdit.Wrap } } } @@ -543,12 +557,11 @@ Frame { } } - Label { + SelectableText { Layout.fillWidth: true text: summaryLayerCard.titleText - font.pixelSize: 12 - font.bold: false - color: "#0f172a" + textColor: "#0f172a" + clip: true } Rectangle { @@ -909,14 +922,14 @@ Frame { radius: 6 implicitHeight: warningLabel.implicitHeight + 12 - Text { + SelectableText { id: warningLabel anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 6 - wrapMode: Text.Wrap - color: "#7a5d10" + textColor: "#7a5d10" + textWrapMode: TextEdit.Wrap text: packetSummaryPane.warningText.length > 0 ? "Warnings\n" + packetSummaryPane.warningText : "" @@ -1008,14 +1021,14 @@ Frame { radius: 6 implicitHeight: streamWarningLabel.implicitHeight + 12 - Text { + SelectableText { id: streamWarningLabel anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: 6 - wrapMode: Text.Wrap - color: "#7a5d10" + textColor: "#7a5d10" + textWrapMode: TextEdit.Wrap text: parent.parent.parent.warningText.length > 0 ? "Warnings\n" + parent.parent.parent.warningText : "" From f195bdb1f0874c6785f2ba0ea4e9bdd7a08ad37b Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 01:15:14 +0500 Subject: [PATCH 05/14] Compact Tauri flow-view header and tighten details tab spacing --- experimental/tauri-ui-spike/web/index.html | 14 +++--- experimental/tauri-ui-spike/web/styles.css | 56 ++++++++++++++++++---- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/experimental/tauri-ui-spike/web/index.html b/experimental/tauri-ui-spike/web/index.html index 35a2a09..8b38f5f 100644 --- a/experimental/tauri-ui-spike/web/index.html +++ b/experimental/tauri-ui-spike/web/index.html @@ -169,13 +169,13 @@
-
- - -
-
-
-

Select a flow to load packets.

+
+
+ + +
+
+

Select a flow to load packets.

diff --git a/experimental/tauri-ui-spike/web/styles.css b/experimental/tauri-ui-spike/web/styles.css index ecf99f2..054b724 100644 --- a/experimental/tauri-ui-spike/web/styles.css +++ b/experimental/tauri-ui-spike/web/styles.css @@ -629,8 +629,8 @@ body.is-resizing-vertical { .tab-button { min-width: 140px; - min-height: 34px; - padding: 7px 16px 8px; + min-height: 30px; + padding: 5px 14px 6px; border: 1px solid var(--border); border-bottom: none; border-radius: 10px 10px 0 0; @@ -767,6 +767,14 @@ body.is-resizing-vertical { padding-right: 2px; } +.packet-details-panel .details-scroll { + gap: 2px; +} + +.packet-details-panel .details-scroll > .status-text:empty { + display: none; +} + .panel-heading { display: flex; align-items: flex-start; @@ -975,20 +983,43 @@ select:disabled { flex-wrap: wrap; } -.subtabbar { +.flow-view-toolbar { display: flex; align-items: flex-end; - gap: 4px; + gap: 10px; margin-bottom: 4px; - flex: 0 0 auto; padding: 0 2px; border-bottom: 1px solid var(--border); + flex: 0 0 auto; + min-width: 0; +} + +.flow-view-status-wrap { + flex: 1 1 auto; + min-width: 0; + padding-bottom: 5px; +} + +.flow-view-status { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.subtabbar { + display: flex; + align-items: flex-end; + gap: 4px; + margin-bottom: -1px; + flex: 0 0 auto; + padding: 0; + border-bottom: none; } .subtab-button { min-width: 96px; - min-height: 30px; - padding: 6px 14px 7px; + min-height: 28px; + padding: 5px 12px 6px; border: 1px solid var(--border); border-bottom: none; border-radius: 10px 10px 0 0; @@ -1055,6 +1086,15 @@ select:disabled { background: var(--surface-muted); } +#packetInspectorView .inspector-tabbar { + margin-bottom: 2px; +} + +#packetInspectorView .inspector-tab { + min-height: 26px; + padding: 4px 11px 5px; +} + .table-wrap { min-height: 0; border: 1px solid var(--border); @@ -1909,7 +1949,7 @@ select:disabled { display: flex; flex: 1 1 auto; flex-direction: column; - gap: 4px; + gap: 2px; min-height: 0; } From 3fa87d75e5aaaf378781dcf5fb8c96162c5907e0 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 01:31:45 +0500 Subject: [PATCH 06/14] Compact Qt lower panel and detail tab spacing --- src/ui/qml/Main.qml | 8 +- src/ui/qml/components/FlowWorkspacePane.qml | 176 ++++++++++++++------ src/ui/qml/components/PacketDetailsPane.qml | 24 +-- src/ui/qml/components/PacketList.qml | 3 +- src/ui/qml/components/StreamView.qml | 5 +- 5 files changed, 145 insertions(+), 71 deletions(-) diff --git a/src/ui/qml/Main.qml b/src/ui/qml/Main.qml index d7cc492..a9f25be 100644 --- a/src/ui/qml/Main.qml +++ b/src/ui/qml/Main.qml @@ -793,7 +793,7 @@ ApplicationWindow { Layout.fillWidth: true currentIndex: mainController.currentTabIndex < 3 ? mainController.currentTabIndex : 0 onCurrentIndexChanged: mainController.currentTabIndex = currentIndex - spacing: 6 + spacing: 4 background: Rectangle { color: "transparent" @@ -801,7 +801,7 @@ ApplicationWindow { TabButton { text: "Flows" - implicitHeight: 36 + implicitHeight: 32 contentItem: Label { text: parent.text @@ -825,7 +825,7 @@ ApplicationWindow { TabButton { text: "Analysis" - implicitHeight: 36 + implicitHeight: 32 contentItem: Label { text: parent.text @@ -849,7 +849,7 @@ ApplicationWindow { TabButton { text: "Statistics" - implicitHeight: 36 + implicitHeight: 32 contentItem: Label { text: parent.text diff --git a/src/ui/qml/components/FlowWorkspacePane.qml b/src/ui/qml/components/FlowWorkspacePane.qml index 3e0daab..9104fee 100644 --- a/src/ui/qml/components/FlowWorkspacePane.qml +++ b/src/ui/qml/components/FlowWorkspacePane.qml @@ -33,6 +33,45 @@ Item { property var selectedPacketIndex: 0 property var selectedStreamItemIndex: 0 readonly property bool selectedFlowWorkspaceLoading: root.selectedFlowIndex >= 0 && (root.packetsLoading || root.streamLoading) + readonly property bool packetsTabSelected: flowDetailTabs.currentIndex === 0 + + function lowerToolbarStatusText() { + if (root.packetsTabSelected) { + if (root.packetsLoading) { + return "Loading packet list..." + } + if (root.totalPacketRowCount > 0) { + return root.packetsPartiallyLoaded + ? "Showing %1 of %2 packets".arg(root.loadedPacketRowCount).arg(root.totalPacketRowCount) + : "Showing all %1 packets".arg(root.totalPacketRowCount) + } + return root.unrecognizedPacketsSelected + ? "Select the unrecognized packets list to inspect packets" + : "Select a flow to inspect packets" + } + + if (!root.sourceCaptureAvailable && root.selectedFlowIndex >= 0) { + return "Source capture unavailable. Reattach the original capture file to inspect stream items." + } + if (root.streamLoading) { + return "Building stream view..." + } + if (root.streamPartiallyLoaded) { + return root.totalStreamItemCount > 0 + ? "Showing %1 of %2 stream items".arg(root.loadedStreamItemCount).arg(root.totalStreamItemCount) + : "Showing first %1 stream items".arg(root.loadedStreamItemCount) + } + if (root.totalStreamItemCount > 0 || root.loadedStreamItemCount > 0) { + let text = "Showing all %1 stream items".arg(root.totalStreamItemCount > 0 ? root.totalStreamItemCount : root.loadedStreamItemCount) + if (root.streamPacketWindowPartial && !root.streamLoading) { + text += " Built from the first %1 packets.".arg(root.streamPacketWindowCount) + } else if (root.canLoadMoreStreamItems && !root.streamLoading) { + text += " Load more packets to extend the stream view." + } + return text + } + return "Select a flow to inspect stream items" + } signal flowSelected(int flowIndex) signal unrecognizedPacketsRequested() @@ -103,71 +142,102 @@ Item { ColumnLayout { anchors.fill: parent - spacing: 6 + spacing: 4 - TabBar { - id: flowDetailTabs + RowLayout { Layout.fillWidth: true - onCurrentIndexChanged: root.flowDetailsTabChanged(currentIndex) - spacing: 6 + spacing: 8 + + TabBar { + id: flowDetailTabs + Layout.preferredWidth: implicitWidth + onCurrentIndexChanged: root.flowDetailsTabChanged(currentIndex) + spacing: 4 - onVisibleChanged: { - if (visible && root.unrecognizedPacketsSelected && currentIndex !== 0) { - currentIndex = 0 - root.flowDetailsTabChanged(0) + onVisibleChanged: { + if (visible && root.unrecognizedPacketsSelected && currentIndex !== 0) { + currentIndex = 0 + root.flowDetailsTabChanged(0) + } } - } - background: Rectangle { - color: "transparent" - } + background: Rectangle { + color: "transparent" + } - TabButton { - text: "Packets" - implicitHeight: 30 + TabButton { + text: "Packets" + implicitHeight: 28 + implicitWidth: 108 - contentItem: Label { - text: parent.text - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 - font.bold: parent.checked - color: parent.checked ? "#0f172a" : "#64748b" - } + contentItem: Label { + text: parent.text + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: 12 + font.bold: parent.checked + color: parent.checked ? "#0f172a" : "#64748b" + } - background: Rectangle { - radius: 6 - color: parent.checked - ? "#ffffff" - : parent.hovered - ? "#f8fafc" - : "#f1f5f9" - border.color: parent.checked ? "#cbd5e1" : "#e2e8f0" + background: Rectangle { + radius: 6 + color: parent.checked + ? "#ffffff" + : parent.hovered + ? "#f8fafc" + : "#f1f5f9" + border.color: parent.checked ? "#cbd5e1" : "#e2e8f0" + } } - } - TabButton { - text: "Stream" - implicitHeight: 30 - enabled: !root.unrecognizedPacketsSelected + TabButton { + text: "Stream" + implicitHeight: 28 + implicitWidth: 108 + enabled: !root.unrecognizedPacketsSelected + + contentItem: Label { + text: parent.text + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: 12 + font.bold: parent.checked + color: parent.checked ? "#0f172a" : "#64748b" + } - contentItem: Label { - text: parent.text - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 - font.bold: parent.checked - color: parent.checked ? "#0f172a" : "#64748b" + background: Rectangle { + radius: 6 + color: parent.checked + ? "#ffffff" + : parent.hovered + ? "#f8fafc" + : "#f1f5f9" + border.color: parent.checked ? "#cbd5e1" : "#e2e8f0" + } } + } - background: Rectangle { - radius: 6 - color: parent.checked - ? "#ffffff" - : parent.hovered - ? "#f8fafc" - : "#f1f5f9" - border.color: parent.checked ? "#cbd5e1" : "#e2e8f0" + Label { + Layout.fillWidth: true + text: root.lowerToolbarStatusText() + color: "#6b7280" + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: 12 + } + + Button { + text: "Load more" + visible: root.packetsTabSelected ? root.canLoadMorePackets : root.canLoadMoreStreamItems + enabled: root.packetsTabSelected + ? (root.canLoadMorePackets && !root.packetsLoading) + : (root.canLoadMoreStreamItems && !root.streamLoading) + onClicked: { + if (root.packetsTabSelected) { + root.loadMorePacketsRequested() + } else { + root.loadMoreStreamItemsRequested() + } } } } @@ -189,6 +259,7 @@ Item { loadedPacketRowCount: root.loadedPacketRowCount totalPacketRowCount: root.totalPacketRowCount canLoadMorePackets: root.canLoadMorePackets + showToolbar: false onPacketSelected: function(packetIndex) { root.packetSelected(packetIndex) } @@ -209,6 +280,7 @@ Item { streamPacketWindowCount: root.streamPacketWindowCount streamPacketWindowPartial: root.streamPacketWindowPartial canLoadMoreStreamItems: root.canLoadMoreStreamItems + showToolbar: false onStreamItemSelected: function(streamItemIndex) { root.streamItemSelected(streamItemIndex) } diff --git a/src/ui/qml/components/PacketDetailsPane.qml b/src/ui/qml/components/PacketDetailsPane.qml index 37087b7..6d09493 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -634,7 +634,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 6 + spacing: 4 Label { text: root.detailsTitle() @@ -708,7 +708,7 @@ Frame { id: packetTabs Layout.fillWidth: true visible: !root.isStreamItemDetails() - spacing: 6 + spacing: 4 background: Rectangle { color: "transparent" @@ -716,7 +716,7 @@ Frame { TabButton { text: "Summary" - implicitHeight: 34 + implicitHeight: 30 contentItem: Label { text: parent.text @@ -740,7 +740,7 @@ Frame { TabButton { text: "Raw" - implicitHeight: 30 + implicitHeight: 28 contentItem: Label { text: parent.text @@ -764,7 +764,7 @@ Frame { TabButton { text: root.payloadTabTitle() - implicitHeight: 30 + implicitHeight: 28 contentItem: Label { text: parent.text @@ -788,7 +788,7 @@ Frame { TabButton { text: "Protocol" - implicitHeight: 30 + implicitHeight: 28 contentItem: Label { text: parent.text @@ -815,7 +815,7 @@ Frame { id: streamTabs Layout.fillWidth: true visible: root.isStreamItemDetails() - spacing: 6 + spacing: 4 background: Rectangle { color: "transparent" @@ -823,7 +823,7 @@ Frame { TabButton { text: "Summary" - implicitHeight: 30 + implicitHeight: 28 contentItem: Label { text: parent.text @@ -847,7 +847,7 @@ Frame { TabButton { text: root.payloadTabTitle() - implicitHeight: 30 + implicitHeight: 28 contentItem: Label { text: parent.text @@ -871,7 +871,7 @@ Frame { TabButton { text: "Protocol" - implicitHeight: 30 + implicitHeight: 28 contentItem: Label { text: parent.text @@ -912,7 +912,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 6 + spacing: 4 Rectangle { Layout.fillWidth: true @@ -1011,7 +1011,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 + spacing: 6 Rectangle { Layout.fillWidth: true diff --git a/src/ui/qml/components/PacketList.qml b/src/ui/qml/components/PacketList.qml index 7e86f6d..b7d7fe9 100644 --- a/src/ui/qml/components/PacketList.qml +++ b/src/ui/qml/components/PacketList.qml @@ -14,6 +14,7 @@ Frame { property var loadedPacketRowCount: 0 property var totalPacketRowCount: 0 property bool canLoadMorePackets: false + property bool showToolbar: true readonly property bool showMarkerColumn: !!root.packetModel && root.packetModel.hasVisibleMarkers readonly property bool unrecognizedMode: !!root.packetModel && root.packetModel.unrecognizedMode readonly property string forwardDirection: "A\u2192B" @@ -168,7 +169,7 @@ Frame { RowLayout { Layout.fillWidth: true - visible: root.packetsLoading || root.totalPacketRowCount > 0 + visible: root.showToolbar && (root.packetsLoading || root.totalPacketRowCount > 0) spacing: 8 Label { diff --git a/src/ui/qml/components/StreamView.qml b/src/ui/qml/components/StreamView.qml index df8891f..9588e71 100644 --- a/src/ui/qml/components/StreamView.qml +++ b/src/ui/qml/components/StreamView.qml @@ -16,6 +16,7 @@ Frame { property int streamPacketWindowCount: 0 property bool streamPacketWindowPartial: false property bool canLoadMoreStreamItems: false + property bool showToolbar: true readonly property string forwardDirection: "A\u2192B" readonly property string reverseDirection: "B\u2192A" @@ -71,11 +72,11 @@ Frame { RowLayout { Layout.fillWidth: true - visible: (!root.sourceCaptureAvailable && root.flowSelected) + visible: root.showToolbar && ((!root.sourceCaptureAvailable && root.flowSelected) || root.streamLoading || root.loadedStreamItemCount > 0 || root.totalStreamItemCount > 0 - || root.streamPacketWindowPartial + || root.streamPacketWindowPartial) spacing: 6 ColumnLayout { From b8b83aecc268ac3734346714a8dfcd913cea41c6 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 01:34:22 +0500 Subject: [PATCH 07/14] Tone down Tauri Open Capture button styling --- experimental/tauri-ui-spike/web/styles.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/experimental/tauri-ui-spike/web/styles.css b/experimental/tauri-ui-spike/web/styles.css index 054b724..5ca4dfa 100644 --- a/experimental/tauri-ui-spike/web/styles.css +++ b/experimental/tauri-ui-spike/web/styles.css @@ -422,6 +422,19 @@ body.is-resizing-vertical { #openFileButton { padding-inline: 14px; + border: 1px solid #b9d7c2; + border-left: 4px solid #5ec47a; + background: #ffffff; + color: var(--text-primary); + font-weight: 600; +} + +#openFileButton:hover:not(:disabled) { + background: #f3fbf5; +} + +#openFileButton:active:not(:disabled) { + background: #e6f6ea; } #openMode { From 92c642650ebd2f48467f9009cccc6de0e28f30e9 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 02:25:22 +0500 Subject: [PATCH 08/14] Fix endpoint formatting and Qt flow table layout regressions --- experimental/tauri-ui-spike/web/main.js | 4 +- experimental/tauri-ui-spike/web/styles.css | 7 +- src/ui/qml/components/FlowTable.qml | 557 ++++++++++---------- src/ui/qml/components/FlowWorkspacePane.qml | 10 +- src/ui/qml/components/PacketDetailsPane.qml | 1 + 5 files changed, 293 insertions(+), 286 deletions(-) diff --git a/experimental/tauri-ui-spike/web/main.js b/experimental/tauri-ui-spike/web/main.js index f649585..097a67e 100644 --- a/experimental/tauri-ui-spike/web/main.js +++ b/experimental/tauri-ui-spike/web/main.js @@ -585,12 +585,12 @@ return ""; } - const displayAddress = trimmedAddress.includes(":") + const displayAddress = hasPort && trimmedAddress.includes(":") ? `[${trimmedAddress}]` : trimmedAddress; return hasPort - ? `${displayAddress} : ${numericPort}` + ? `${displayAddress} : ${numericPort}` : displayAddress; } diff --git a/experimental/tauri-ui-spike/web/styles.css b/experimental/tauri-ui-spike/web/styles.css index 5ca4dfa..dc258a7 100644 --- a/experimental/tauri-ui-spike/web/styles.css +++ b/experimental/tauri-ui-spike/web/styles.css @@ -1209,18 +1209,13 @@ select:disabled { .endpoint-cell-inner { display: inline-grid; - grid-template-columns: minmax(0, 16ch) 1ch 5ch; + grid-template-columns: max-content 1ch 5ch; align-items: center; column-gap: 0.35rem; justify-content: start; - max-width: 100%; - min-width: 0; } .endpoint-address { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; } diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index cad67ba..fb4c39f 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -22,14 +22,28 @@ Frame { readonly property int familyColumnWidth: 74 readonly property int protocolColumnWidth: 86 readonly property int protocolHintColumnWidth: 98 - readonly property int serviceColumnWidth: 200 - readonly property int endpointColumnWidth: 220 - readonly property int endpointAddressSlotWidth: 136 - readonly property int endpointPortWidth: 44 - readonly property int endpointSeparatorWidth: 10 + readonly property int serviceColumnWidth: 180 + readonly property int endpointColumnWidth: Math.ceil(endpointTextMetrics.width) + 16 readonly property int fragColumnWidth: 56 readonly property int packetsColumnWidth: 86 readonly property int bytesColumnWidth: 92 + readonly property int flowTableColumnCount: 11 + readonly property int flowTableBaseWidth: + root.tableContentLeftMargin + + root.tableContentRightMargin + + root.selectionColumnWidth + + root.indexColumnWidth + + root.familyColumnWidth + + root.protocolColumnWidth + + root.protocolHintColumnWidth + + root.serviceColumnWidth + + root.endpointColumnWidth + + root.endpointColumnWidth + + root.fragColumnWidth + + root.packetsColumnWidth + + root.bytesColumnWidth + + root.tableRowSpacing * (root.flowTableColumnCount - 1) + readonly property int flowTableContentWidth: root.flowTableBaseWidth + flowListView.rightGutter signal flowSelected(int flowIndex) signal filterTextEdited(string text) @@ -91,12 +105,12 @@ Frame { return "" } - const displayAddress = trimmedAddress.indexOf(":") >= 0 + const displayAddress = hasPort && trimmedAddress.indexOf(":") >= 0 ? "[" + trimmedAddress + "]" : trimmedAddress return hasPort - ? displayAddress + " : " + numericPort + ? displayAddress + " : " + numericPort : displayAddress } @@ -124,6 +138,12 @@ Frame { radius: 8 } + TextMetrics { + id: endpointTextMetrics + font.family: "Consolas" + text: "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff] : 65535" + } + onFlowModelChanged: syncSelectedFlowRow() onSelectedFlowIndexChanged: syncSelectedFlowRow() onVisibleChanged: { @@ -197,321 +217,304 @@ Frame { } } - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: root.tableContentLeftMargin - Layout.rightMargin: root.tableContentRightMargin + flowListView.rightGutter - spacing: root.tableRowSpacing - - Label { text: "Sel"; Layout.preferredWidth: root.selectionColumnWidth; horizontalAlignment: Text.AlignHCenter } - Button { text: "Index" + root.sortIndicator(0); Layout.preferredWidth: root.indexColumnWidth; onClicked: root.sortRequested(0) } - Button { text: "Family" + root.sortIndicator(1); Layout.preferredWidth: root.familyColumnWidth; onClicked: root.sortRequested(1) } - Button { text: "Protocol" + root.sortIndicator(2); Layout.preferredWidth: root.protocolColumnWidth; onClicked: root.sortRequested(2) } - Button { text: "Proto Hint" + root.sortIndicator(3); Layout.preferredWidth: root.protocolHintColumnWidth; onClicked: root.sortRequested(3) } - Button { text: "Service" + root.sortIndicator(4); Layout.fillWidth: true; Layout.preferredWidth: root.serviceColumnWidth; onClicked: root.sortRequested(4) } - Button { text: "Endpoint A" + root.sortIndicator(6); Layout.fillWidth: true; Layout.preferredWidth: root.endpointColumnWidth; onClicked: root.sortRequested(6) } - Button { text: "Endpoint B" + root.sortIndicator(8); Layout.fillWidth: true; Layout.preferredWidth: root.endpointColumnWidth; onClicked: root.sortRequested(8) } - Button { text: "Frag" + root.sortIndicator(5); Layout.preferredWidth: root.fragColumnWidth; onClicked: root.sortRequested(5) } - Button { text: "Packets" + root.sortIndicator(10); Layout.preferredWidth: root.packetsColumnWidth; onClicked: root.sortRequested(10) } - Button { text: "Bytes" + root.sortIndicator(11); Layout.preferredWidth: root.bytesColumnWidth; onClicked: root.sortRequested(11) } - } - - Rectangle { + Flickable { + id: flowTableScroller Layout.fillWidth: true Layout.fillHeight: true - color: "#f8fafc" - border.color: "#e2e8f0" - radius: 6 - - ListView { - id: flowListView - readonly property int rightGutter: flowScrollBar.visible ? flowScrollBar.width + 10 : 0 - - anchors.fill: parent - anchors.margins: 1 - clip: true - model: root.flowModel - currentIndex: -1 - onCountChanged: root.syncSelectedFlowRow() - onModelChanged: root.syncSelectedFlowRow() - - ScrollBar.vertical: ScrollBar { - id: flowScrollBar - policy: flowListView.contentHeight > flowListView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff - } + clip: true + contentWidth: flowTableContent.width + contentHeight: flowTableContent.height + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.horizontal: ScrollBar { + id: flowTableHorizontalScrollBar + policy: flowTableScroller.contentWidth > flowTableScroller.width ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff + } - delegate: Rectangle { - id: flowRow - required property int index - required property int flowIndex - required property bool flowChecked - required property string family - required property string protocol - required property string protocolHint - required property string serviceHint - required property bool hasFragmentedPackets - required property string fragmentedPacketCount - required property string addressA - required property int portA - required property string addressB - required property int portB - required property string packets - required property string bytes - - readonly property bool selected: index === flowListView.currentIndex - readonly property bool endpointAHasPort: root.endpointHasPort(portA) - readonly property bool endpointBHasPort: root.endpointHasPort(portB) - readonly property string endpointAAddressText: root.endpointDisplayAddress(addressA, portA) - readonly property string endpointBAddressText: root.endpointDisplayAddress(addressB, portB) - readonly property string endpointAText: root.formatEndpoint(addressA, portA) - readonly property string endpointBText: root.formatEndpoint(addressB, portB) - - onFlowCheckedChanged: { - if (selectionCheckBox.checked !== flowChecked) { - selectionCheckBox.checked = flowChecked - } - } + Item { + id: flowTableContent + width: Math.max(flowTableScroller.width, root.flowTableContentWidth) + height: flowHeaderContainer.height + flowBodyContainer.height - width: flowListView.width - height: 32 - color: selected - ? "#dbeafe" - : (index % 2 === 0 ? "#ffffff" : "#f8fafc") + Item { + id: flowHeaderContainer + width: parent.width + height: flowHeaderRow.implicitHeight + clip: true RowLayout { + id: flowHeaderRow anchors.fill: parent anchors.leftMargin: root.tableContentLeftMargin anchors.rightMargin: root.tableContentRightMargin + flowListView.rightGutter spacing: root.tableRowSpacing - Item { - Layout.preferredWidth: root.selectionColumnWidth - Layout.fillHeight: true - - CheckBox { - id: selectionCheckBox - anchors.centerIn: parent - checked: flowChecked - onToggled: function() { - if (root.flowModel && checked !== flowChecked) { - root.flowModel.setFlowChecked(flowIndex, checked) - } - } - } - } - - Text { - text: flowIndex + 1 - Layout.preferredWidth: root.indexColumnWidth - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - } - Text { - text: family - Layout.preferredWidth: root.familyColumnWidth - verticalAlignment: Text.AlignVCenter - } - Text { - text: protocol - Layout.preferredWidth: root.protocolColumnWidth - verticalAlignment: Text.AlignVCenter - } - Item { - Layout.preferredWidth: root.protocolHintColumnWidth - implicitHeight: protocolHintLabel.implicitHeight + Label { text: "Sel"; Layout.preferredWidth: root.selectionColumnWidth; horizontalAlignment: Text.AlignHCenter } + Button { text: "Index" + root.sortIndicator(0); Layout.preferredWidth: root.indexColumnWidth; onClicked: root.sortRequested(0) } + Button { text: "Family" + root.sortIndicator(1); Layout.preferredWidth: root.familyColumnWidth; onClicked: root.sortRequested(1) } + Button { text: "Protocol" + root.sortIndicator(2); Layout.preferredWidth: root.protocolColumnWidth; onClicked: root.sortRequested(2) } + Button { text: "Proto Hint" + root.sortIndicator(3); Layout.preferredWidth: root.protocolHintColumnWidth; onClicked: root.sortRequested(3) } + Button { text: "Service" + root.sortIndicator(4); Layout.preferredWidth: root.serviceColumnWidth; Layout.minimumWidth: root.serviceColumnWidth; Layout.maximumWidth: root.serviceColumnWidth; onClicked: root.sortRequested(4) } + Button { text: "Endpoint A" + root.sortIndicator(6); Layout.preferredWidth: root.endpointColumnWidth; Layout.minimumWidth: root.endpointColumnWidth; Layout.maximumWidth: root.endpointColumnWidth; onClicked: root.sortRequested(6) } + Button { text: "Endpoint B" + root.sortIndicator(8); Layout.preferredWidth: root.endpointColumnWidth; Layout.minimumWidth: root.endpointColumnWidth; Layout.maximumWidth: root.endpointColumnWidth; onClicked: root.sortRequested(8) } + Button { text: "Frag" + root.sortIndicator(5); Layout.preferredWidth: root.fragColumnWidth; onClicked: root.sortRequested(5) } + Button { text: "Packets" + root.sortIndicator(10); Layout.preferredWidth: root.packetsColumnWidth; onClicked: root.sortRequested(10) } + Button { text: "Bytes" + root.sortIndicator(11); Layout.preferredWidth: root.bytesColumnWidth; onClicked: root.sortRequested(11) } + } + } - Label { - id: protocolHintLabel - anchors.fill: parent - text: protocolHint - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } + Rectangle { + id: flowBodyContainer + y: flowHeaderContainer.height + width: parent.width + height: Math.max(0, flowTableScroller.height - flowHeaderContainer.height - (flowTableHorizontalScrollBar.visible ? flowTableHorizontalScrollBar.height : 0)) + color: "#f8fafc" + border.color: "#e2e8f0" + radius: 6 - MouseArea { - id: protocolHintHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true - } + ListView { + id: flowListView + readonly property int rightGutter: flowScrollBar.visible ? flowScrollBar.width + 10 : 0 - ToolTip.visible: protocolHintHoverArea.containsMouse && protocolHintLabel.truncated - ToolTip.text: protocolHintLabel.text + anchors.fill: parent + anchors.margins: 1 + clip: true + model: root.flowModel + currentIndex: -1 + onCountChanged: root.syncSelectedFlowRow() + onModelChanged: root.syncSelectedFlowRow() + + ScrollBar.vertical: ScrollBar { + id: flowScrollBar + policy: flowListView.contentHeight > flowListView.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff } - Item { - Layout.fillWidth: true - Layout.preferredWidth: root.serviceColumnWidth - implicitHeight: serviceHintLabel.implicitHeight - - Label { - id: serviceHintLabel - anchors.fill: parent - text: serviceHint - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - MouseArea { - id: serviceHintHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true + delegate: Rectangle { + id: flowRow + required property int index + required property int flowIndex + required property bool flowChecked + required property string family + required property string protocol + required property string protocolHint + required property string serviceHint + required property bool hasFragmentedPackets + required property string fragmentedPacketCount + required property string addressA + required property int portA + required property string addressB + required property int portB + required property string packets + required property string bytes + + readonly property bool selected: index === flowListView.currentIndex + readonly property string endpointAText: root.formatEndpoint(addressA, portA) + readonly property string endpointBText: root.formatEndpoint(addressB, portB) + + onFlowCheckedChanged: { + if (selectionCheckBox.checked !== flowChecked) { + selectionCheckBox.checked = flowChecked + } } - ToolTip.visible: serviceHintHoverArea.containsMouse && serviceHintLabel.truncated - ToolTip.text: serviceHintLabel.text - } - - Item { - Layout.fillWidth: true - Layout.preferredWidth: root.endpointColumnWidth - implicitHeight: endpointARow.implicitHeight + width: flowListView.width + height: 32 + clip: true + color: selected + ? "#dbeafe" + : (index % 2 === 0 ? "#ffffff" : "#f8fafc") RowLayout { - id: endpointARow - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - spacing: 0 - - Label { - id: endpointALabel - Layout.preferredWidth: root.endpointAddressSlotWidth - text: endpointAAddressText - font.family: "Consolas" - elide: Text.ElideMiddle + anchors.fill: parent + anchors.leftMargin: root.tableContentLeftMargin + anchors.rightMargin: root.tableContentRightMargin + flowListView.rightGutter + spacing: root.tableRowSpacing + + Item { + Layout.preferredWidth: root.selectionColumnWidth + Layout.fillHeight: true + + CheckBox { + id: selectionCheckBox + anchors.centerIn: parent + checked: flowChecked + onToggled: function() { + if (root.flowModel && checked !== flowChecked) { + root.flowModel.setFlowChecked(flowIndex, checked) + } + } + } + } + + Text { + text: flowIndex + 1 + Layout.preferredWidth: root.indexColumnWidth + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + Text { + text: family + Layout.preferredWidth: root.familyColumnWidth verticalAlignment: Text.AlignVCenter } + Text { + text: protocol + Layout.preferredWidth: root.protocolColumnWidth + verticalAlignment: Text.AlignVCenter + } + Item { + Layout.preferredWidth: root.protocolHintColumnWidth + implicitHeight: protocolHintLabel.implicitHeight + clip: true + + Label { + id: protocolHintLabel + anchors.fill: parent + text: protocolHint + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - Label { - visible: endpointAHasPort - Layout.preferredWidth: root.endpointSeparatorWidth - horizontalAlignment: Text.AlignHCenter - text: ":" - font.family: "Consolas" - color: "#64748b" + MouseArea { + id: protocolHintHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } + + ToolTip.visible: protocolHintHoverArea.containsMouse && protocolHintLabel.truncated + ToolTip.text: protocolHintLabel.text } + Item { + Layout.preferredWidth: root.serviceColumnWidth + Layout.minimumWidth: root.serviceColumnWidth + Layout.maximumWidth: root.serviceColumnWidth + implicitHeight: serviceHintLabel.implicitHeight + clip: true + + Label { + id: serviceHintLabel + anchors.fill: parent + text: serviceHint + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - Label { - visible: endpointAHasPort - Layout.preferredWidth: root.endpointPortWidth - horizontalAlignment: Text.AlignRight - text: portA - font.family: "Consolas" - color: "#0f172a" + MouseArea { + id: serviceHintHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } + + ToolTip.visible: serviceHintHoverArea.containsMouse && serviceHintLabel.truncated + ToolTip.text: serviceHintLabel.text } - } - MouseArea { - id: endpointAHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true - } + Item { + Layout.preferredWidth: root.endpointColumnWidth + Layout.minimumWidth: root.endpointColumnWidth + Layout.maximumWidth: root.endpointColumnWidth + implicitHeight: endpointALabel.implicitHeight + clip: true + + Label { + id: endpointALabel + anchors.fill: parent + text: endpointAText + font.family: "Consolas" + verticalAlignment: Text.AlignVCenter + } - ToolTip.visible: endpointAHoverArea.containsMouse && endpointALabel.truncated - ToolTip.text: endpointAText - } - Item { - Layout.fillWidth: true - Layout.preferredWidth: root.endpointColumnWidth - implicitHeight: endpointBRow.implicitHeight + MouseArea { + id: endpointAHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } - RowLayout { - id: endpointBRow - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - spacing: 0 - - Label { - id: endpointBLabel - Layout.preferredWidth: root.endpointAddressSlotWidth - text: endpointBAddressText - font.family: "Consolas" - elide: Text.ElideMiddle - verticalAlignment: Text.AlignVCenter + ToolTip.visible: endpointAHoverArea.containsMouse && endpointAText.length > 0 + ToolTip.text: endpointAText } + Item { + Layout.preferredWidth: root.endpointColumnWidth + Layout.minimumWidth: root.endpointColumnWidth + Layout.maximumWidth: root.endpointColumnWidth + implicitHeight: endpointBLabel.implicitHeight + clip: true + + Label { + id: endpointBLabel + anchors.fill: parent + text: endpointBText + font.family: "Consolas" + verticalAlignment: Text.AlignVCenter + } - Label { - visible: endpointBHasPort - Layout.preferredWidth: root.endpointSeparatorWidth - horizontalAlignment: Text.AlignHCenter - text: ":" - font.family: "Consolas" - color: "#64748b" - } + MouseArea { + id: endpointBHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } - Label { - visible: endpointBHasPort - Layout.preferredWidth: root.endpointPortWidth + ToolTip.visible: endpointBHoverArea.containsMouse && endpointBText.length > 0 + ToolTip.text: endpointBText + } + Rectangle { + Layout.preferredWidth: root.fragColumnWidth + implicitHeight: 20 + radius: 4 + color: root.fragBackgroundColor(hasFragmentedPackets, selected) + border.width: color === "transparent" ? 0 : 1 + border.color: color === "transparent" ? "transparent" : Qt.darker(color, 1.08) + + Text { + anchors.centerIn: parent + width: parent.width + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: fragmentedPacketCount + color: root.fragTextColor(hasFragmentedPackets, selected) + } + } + Text { + text: packets + Layout.preferredWidth: root.packetsColumnWidth horizontalAlignment: Text.AlignRight - text: portB - font.family: "Consolas" - color: "#0f172a" + verticalAlignment: Text.AlignVCenter + } + Text { + text: bytes + Layout.preferredWidth: root.bytesColumnWidth + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter } } MouseArea { - id: endpointBHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 52 hoverEnabled: true + onClicked: { + flowListView.currentIndex = index + root.flowSelected(flowIndex) + } } - - ToolTip.visible: endpointBHoverArea.containsMouse && endpointBLabel.truncated - ToolTip.text: endpointBText - } - Rectangle { - Layout.preferredWidth: root.fragColumnWidth - implicitHeight: 20 - radius: 4 - color: root.fragBackgroundColor(hasFragmentedPackets, selected) - border.width: color === "transparent" ? 0 : 1 - border.color: color === "transparent" ? "transparent" : Qt.darker(color, 1.08) - - Text { - anchors.centerIn: parent - width: parent.width - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: fragmentedPacketCount - color: root.fragTextColor(hasFragmentedPackets, selected) - } - } - Text { - text: packets - Layout.preferredWidth: root.packetsColumnWidth - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - } - Text { - text: bytes - Layout.preferredWidth: root.bytesColumnWidth - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter } } - MouseArea { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 52 - hoverEnabled: true - onClicked: { - flowListView.currentIndex = index - root.flowSelected(flowIndex) - } + Label { + anchors.centerIn: parent + visible: flowListView.count === 0 + color: "#64748b" + text: "No flows loaded" } } } - - Label { - anchors.centerIn: parent - visible: flowListView.count === 0 - color: "#64748b" - text: "No flows loaded" - } } Rectangle { diff --git a/src/ui/qml/components/FlowWorkspacePane.qml b/src/ui/qml/components/FlowWorkspacePane.qml index 9104fee..d5f97d7 100644 --- a/src/ui/qml/components/FlowWorkspacePane.qml +++ b/src/ui/qml/components/FlowWorkspacePane.qml @@ -35,6 +35,14 @@ Item { readonly property bool selectedFlowWorkspaceLoading: root.selectedFlowIndex >= 0 && (root.packetsLoading || root.streamLoading) readonly property bool packetsTabSelected: flowDetailTabs.currentIndex === 0 + function lowerToolbarStatusColor() { + if (!root.packetsTabSelected && !root.sourceCaptureAvailable && root.selectedFlowIndex >= 0) { + return "#8a6a12" + } + + return "#6b7280" + } + function lowerToolbarStatusText() { if (root.packetsTabSelected) { if (root.packetsLoading) { @@ -220,7 +228,7 @@ Item { Label { Layout.fillWidth: true text: root.lowerToolbarStatusText() - color: "#6b7280" + color: root.lowerToolbarStatusColor() elide: Text.ElideRight verticalAlignment: Text.AlignVCenter font.pixelSize: 12 diff --git a/src/ui/qml/components/PacketDetailsPane.qml b/src/ui/qml/components/PacketDetailsPane.qml index 6d09493..64d9e82 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -438,6 +438,7 @@ Frame { property int textWrapMode: TextEdit.NoWrap readOnly: true + activeFocusOnTab: false selectByMouse: true textFormat: TextEdit.PlainText wrapMode: textWrapMode From cf14e1ce3cf15ff37f98e16c7606523aa84dee53 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 02:43:27 +0500 Subject: [PATCH 09/14] Clean up Qt flow table endpoint elision and scrollbar layout --- src/ui/qml/components/FlowTable.qml | 385 ++++++++++++++-------------- 1 file changed, 192 insertions(+), 193 deletions(-) diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index fb4c39f..ce48e4c 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -114,24 +114,6 @@ Frame { : displayAddress } - function endpointHasPort(port) { - const numericPort = Number(port) - return Number.isFinite(numericPort) && numericPort > 0 - } - - function endpointDisplayAddress(address, port) { - const trimmedAddress = address ? String(address).trim() : "" - if (trimmedAddress.length === 0) { - return "" - } - - if (root.endpointHasPort(port) && trimmedAddress.indexOf(":") >= 0) { - return "[" + trimmedAddress + "]" - } - - return trimmedAddress - } - background: Rectangle { color: "#ffffff" border.color: "#d8dee9" @@ -217,24 +199,28 @@ Frame { } } - Flickable { - id: flowTableScroller + Item { + id: flowTableViewport Layout.fillWidth: true Layout.fillHeight: true - clip: true - contentWidth: flowTableContent.width - contentHeight: flowTableContent.height - boundsBehavior: Flickable.StopAtBounds - - ScrollBar.horizontal: ScrollBar { - id: flowTableHorizontalScrollBar - policy: flowTableScroller.contentWidth > flowTableScroller.width ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff + + Flickable { + id: flowTableScroller + anchors.fill: parent + clip: true + contentWidth: root.flowTableContentWidth + contentHeight: 1 + flickableDirection: Flickable.HorizontalFlick + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.horizontal: ScrollBar { + id: flowTableHorizontalScrollBar + policy: flowTableScroller.contentWidth > flowTableScroller.width ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff + } } Item { - id: flowTableContent - width: Math.max(flowTableScroller.width, root.flowTableContentWidth) - height: flowHeaderContainer.height + flowBodyContainer.height + anchors.fill: parent Item { id: flowHeaderContainer @@ -242,24 +228,30 @@ Frame { height: flowHeaderRow.implicitHeight clip: true - RowLayout { - id: flowHeaderRow - anchors.fill: parent - anchors.leftMargin: root.tableContentLeftMargin - anchors.rightMargin: root.tableContentRightMargin + flowListView.rightGutter - spacing: root.tableRowSpacing - - Label { text: "Sel"; Layout.preferredWidth: root.selectionColumnWidth; horizontalAlignment: Text.AlignHCenter } - Button { text: "Index" + root.sortIndicator(0); Layout.preferredWidth: root.indexColumnWidth; onClicked: root.sortRequested(0) } - Button { text: "Family" + root.sortIndicator(1); Layout.preferredWidth: root.familyColumnWidth; onClicked: root.sortRequested(1) } - Button { text: "Protocol" + root.sortIndicator(2); Layout.preferredWidth: root.protocolColumnWidth; onClicked: root.sortRequested(2) } - Button { text: "Proto Hint" + root.sortIndicator(3); Layout.preferredWidth: root.protocolHintColumnWidth; onClicked: root.sortRequested(3) } - Button { text: "Service" + root.sortIndicator(4); Layout.preferredWidth: root.serviceColumnWidth; Layout.minimumWidth: root.serviceColumnWidth; Layout.maximumWidth: root.serviceColumnWidth; onClicked: root.sortRequested(4) } - Button { text: "Endpoint A" + root.sortIndicator(6); Layout.preferredWidth: root.endpointColumnWidth; Layout.minimumWidth: root.endpointColumnWidth; Layout.maximumWidth: root.endpointColumnWidth; onClicked: root.sortRequested(6) } - Button { text: "Endpoint B" + root.sortIndicator(8); Layout.preferredWidth: root.endpointColumnWidth; Layout.minimumWidth: root.endpointColumnWidth; Layout.maximumWidth: root.endpointColumnWidth; onClicked: root.sortRequested(8) } - Button { text: "Frag" + root.sortIndicator(5); Layout.preferredWidth: root.fragColumnWidth; onClicked: root.sortRequested(5) } - Button { text: "Packets" + root.sortIndicator(10); Layout.preferredWidth: root.packetsColumnWidth; onClicked: root.sortRequested(10) } - Button { text: "Bytes" + root.sortIndicator(11); Layout.preferredWidth: root.bytesColumnWidth; onClicked: root.sortRequested(11) } + Item { + x: -flowTableScroller.contentX + width: root.flowTableContentWidth + height: parent.height + + RowLayout { + id: flowHeaderRow + anchors.fill: parent + anchors.leftMargin: root.tableContentLeftMargin + anchors.rightMargin: root.tableContentRightMargin + flowListView.rightGutter + spacing: root.tableRowSpacing + + Label { text: "Sel"; Layout.preferredWidth: root.selectionColumnWidth; horizontalAlignment: Text.AlignHCenter } + Button { text: "Index" + root.sortIndicator(0); Layout.preferredWidth: root.indexColumnWidth; onClicked: root.sortRequested(0) } + Button { text: "Family" + root.sortIndicator(1); Layout.preferredWidth: root.familyColumnWidth; onClicked: root.sortRequested(1) } + Button { text: "Protocol" + root.sortIndicator(2); Layout.preferredWidth: root.protocolColumnWidth; onClicked: root.sortRequested(2) } + Button { text: "Proto Hint" + root.sortIndicator(3); Layout.preferredWidth: root.protocolHintColumnWidth; onClicked: root.sortRequested(3) } + Button { text: "Service" + root.sortIndicator(4); Layout.preferredWidth: root.serviceColumnWidth; Layout.minimumWidth: root.serviceColumnWidth; Layout.maximumWidth: root.serviceColumnWidth; onClicked: root.sortRequested(4) } + Button { text: "Endpoint A" + root.sortIndicator(6); Layout.preferredWidth: root.endpointColumnWidth; Layout.minimumWidth: root.endpointColumnWidth; Layout.maximumWidth: root.endpointColumnWidth; onClicked: root.sortRequested(6) } + Button { text: "Endpoint B" + root.sortIndicator(8); Layout.preferredWidth: root.endpointColumnWidth; Layout.minimumWidth: root.endpointColumnWidth; Layout.maximumWidth: root.endpointColumnWidth; onClicked: root.sortRequested(8) } + Button { text: "Frag" + root.sortIndicator(5); Layout.preferredWidth: root.fragColumnWidth; onClicked: root.sortRequested(5) } + Button { text: "Packets" + root.sortIndicator(10); Layout.preferredWidth: root.packetsColumnWidth; onClicked: root.sortRequested(10) } + Button { text: "Bytes" + root.sortIndicator(11); Layout.preferredWidth: root.bytesColumnWidth; onClicked: root.sortRequested(11) } + } } } @@ -267,7 +259,7 @@ Frame { id: flowBodyContainer y: flowHeaderContainer.height width: parent.width - height: Math.max(0, flowTableScroller.height - flowHeaderContainer.height - (flowTableHorizontalScrollBar.visible ? flowTableHorizontalScrollBar.height : 0)) + height: Math.max(0, parent.height - flowHeaderContainer.height - (flowTableHorizontalScrollBar.visible ? flowTableHorizontalScrollBar.height : 0)) color: "#f8fafc" border.color: "#e2e8f0" radius: 6 @@ -324,171 +316,178 @@ Frame { ? "#dbeafe" : (index % 2 === 0 ? "#ffffff" : "#f8fafc") - RowLayout { - anchors.fill: parent - anchors.leftMargin: root.tableContentLeftMargin - anchors.rightMargin: root.tableContentRightMargin + flowListView.rightGutter - spacing: root.tableRowSpacing - - Item { - Layout.preferredWidth: root.selectionColumnWidth - Layout.fillHeight: true - - CheckBox { - id: selectionCheckBox - anchors.centerIn: parent - checked: flowChecked - onToggled: function() { - if (root.flowModel && checked !== flowChecked) { - root.flowModel.setFlowChecked(flowIndex, checked) + Item { + x: -flowTableScroller.contentX + width: root.flowTableContentWidth + height: parent.height + + RowLayout { + anchors.fill: parent + anchors.leftMargin: root.tableContentLeftMargin + anchors.rightMargin: root.tableContentRightMargin + flowListView.rightGutter + spacing: root.tableRowSpacing + + Item { + Layout.preferredWidth: root.selectionColumnWidth + Layout.fillHeight: true + + CheckBox { + id: selectionCheckBox + anchors.centerIn: parent + checked: flowChecked + onToggled: function() { + if (root.flowModel && checked !== flowChecked) { + root.flowModel.setFlowChecked(flowIndex, checked) + } } } } - } - Text { - text: flowIndex + 1 - Layout.preferredWidth: root.indexColumnWidth - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - } - Text { - text: family - Layout.preferredWidth: root.familyColumnWidth - verticalAlignment: Text.AlignVCenter - } - Text { - text: protocol - Layout.preferredWidth: root.protocolColumnWidth - verticalAlignment: Text.AlignVCenter - } - Item { - Layout.preferredWidth: root.protocolHintColumnWidth - implicitHeight: protocolHintLabel.implicitHeight - clip: true - - Label { - id: protocolHintLabel - anchors.fill: parent - text: protocolHint - elide: Text.ElideRight + Text { + text: flowIndex + 1 + Layout.preferredWidth: root.indexColumnWidth + horizontalAlignment: Text.AlignRight verticalAlignment: Text.AlignVCenter } - - MouseArea { - id: protocolHintHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true - } - - ToolTip.visible: protocolHintHoverArea.containsMouse && protocolHintLabel.truncated - ToolTip.text: protocolHintLabel.text - } - Item { - Layout.preferredWidth: root.serviceColumnWidth - Layout.minimumWidth: root.serviceColumnWidth - Layout.maximumWidth: root.serviceColumnWidth - implicitHeight: serviceHintLabel.implicitHeight - clip: true - - Label { - id: serviceHintLabel - anchors.fill: parent - text: serviceHint - elide: Text.ElideRight + Text { + text: family + Layout.preferredWidth: root.familyColumnWidth verticalAlignment: Text.AlignVCenter } - - MouseArea { - id: serviceHintHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true + Text { + text: protocol + Layout.preferredWidth: root.protocolColumnWidth + verticalAlignment: Text.AlignVCenter } + Item { + Layout.preferredWidth: root.protocolHintColumnWidth + implicitHeight: protocolHintLabel.implicitHeight + clip: true + + Label { + id: protocolHintLabel + anchors.fill: parent + text: protocolHint + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - ToolTip.visible: serviceHintHoverArea.containsMouse && serviceHintLabel.truncated - ToolTip.text: serviceHintLabel.text - } + MouseArea { + id: protocolHintHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } - Item { - Layout.preferredWidth: root.endpointColumnWidth - Layout.minimumWidth: root.endpointColumnWidth - Layout.maximumWidth: root.endpointColumnWidth - implicitHeight: endpointALabel.implicitHeight - clip: true - - Label { - id: endpointALabel - anchors.fill: parent - text: endpointAText - font.family: "Consolas" - verticalAlignment: Text.AlignVCenter + ToolTip.visible: protocolHintHoverArea.containsMouse && protocolHintLabel.truncated + ToolTip.text: protocolHintLabel.text } + Item { + Layout.preferredWidth: root.serviceColumnWidth + Layout.minimumWidth: root.serviceColumnWidth + Layout.maximumWidth: root.serviceColumnWidth + implicitHeight: serviceHintLabel.implicitHeight + clip: true + + Label { + id: serviceHintLabel + anchors.fill: parent + text: serviceHint + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - MouseArea { - id: endpointAHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true - } + MouseArea { + id: serviceHintHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } - ToolTip.visible: endpointAHoverArea.containsMouse && endpointAText.length > 0 - ToolTip.text: endpointAText - } - Item { - Layout.preferredWidth: root.endpointColumnWidth - Layout.minimumWidth: root.endpointColumnWidth - Layout.maximumWidth: root.endpointColumnWidth - implicitHeight: endpointBLabel.implicitHeight - clip: true - - Label { - id: endpointBLabel - anchors.fill: parent - text: endpointBText - font.family: "Consolas" - verticalAlignment: Text.AlignVCenter + ToolTip.visible: serviceHintHoverArea.containsMouse && serviceHintLabel.truncated + ToolTip.text: serviceHintLabel.text } + Item { + Layout.preferredWidth: root.endpointColumnWidth + Layout.minimumWidth: root.endpointColumnWidth + Layout.maximumWidth: root.endpointColumnWidth + implicitHeight: endpointALabel.implicitHeight + clip: true + + Label { + id: endpointALabel + anchors.fill: parent + text: endpointAText + font.family: "Consolas" + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - MouseArea { - id: endpointBHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true + MouseArea { + id: endpointAHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } + + ToolTip.visible: endpointAHoverArea.containsMouse && endpointAText.length > 0 + ToolTip.text: endpointAText } + Item { + Layout.preferredWidth: root.endpointColumnWidth + Layout.minimumWidth: root.endpointColumnWidth + Layout.maximumWidth: root.endpointColumnWidth + implicitHeight: endpointBLabel.implicitHeight + clip: true + + Label { + id: endpointBLabel + anchors.fill: parent + text: endpointBText + font.family: "Consolas" + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - ToolTip.visible: endpointBHoverArea.containsMouse && endpointBText.length > 0 - ToolTip.text: endpointBText - } - Rectangle { - Layout.preferredWidth: root.fragColumnWidth - implicitHeight: 20 - radius: 4 - color: root.fragBackgroundColor(hasFragmentedPackets, selected) - border.width: color === "transparent" ? 0 : 1 - border.color: color === "transparent" ? "transparent" : Qt.darker(color, 1.08) + MouseArea { + id: endpointBHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } + ToolTip.visible: endpointBHoverArea.containsMouse && endpointBText.length > 0 + ToolTip.text: endpointBText + } + Rectangle { + Layout.preferredWidth: root.fragColumnWidth + implicitHeight: 20 + radius: 4 + color: root.fragBackgroundColor(hasFragmentedPackets, selected) + border.width: color === "transparent" ? 0 : 1 + border.color: color === "transparent" ? "transparent" : Qt.darker(color, 1.08) + + Text { + anchors.centerIn: parent + width: parent.width + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: fragmentedPacketCount + color: root.fragTextColor(hasFragmentedPackets, selected) + } + } Text { - anchors.centerIn: parent - width: parent.width - horizontalAlignment: Text.AlignHCenter + text: packets + Layout.preferredWidth: root.packetsColumnWidth + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + } + Text { + text: bytes + Layout.preferredWidth: root.bytesColumnWidth + horizontalAlignment: Text.AlignRight verticalAlignment: Text.AlignVCenter - text: fragmentedPacketCount - color: root.fragTextColor(hasFragmentedPackets, selected) } - } - Text { - text: packets - Layout.preferredWidth: root.packetsColumnWidth - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - } - Text { - text: bytes - Layout.preferredWidth: root.bytesColumnWidth - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter } } From 2fa33b855f05cfc2b1b9f3fd4ff259e13901589a Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 03:09:34 +0500 Subject: [PATCH 10/14] Finalize Qt endpoint tooltip cleanup and keep selectable text stable --- src/ui/qml/components/FlowTable.qml | 8 ++++++-- src/ui/qml/components/PacketDetailsPane.qml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index ce48e4c..a859a4a 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -430,7 +430,9 @@ Frame { hoverEnabled: true } - ToolTip.visible: endpointAHoverArea.containsMouse && endpointAText.length > 0 + ToolTip.visible: endpointAHoverArea.containsMouse + && endpointAText.length > 0 + && endpointALabel.implicitWidth > endpointALabel.width + 1 ToolTip.text: endpointAText } Item { @@ -456,7 +458,9 @@ Frame { hoverEnabled: true } - ToolTip.visible: endpointBHoverArea.containsMouse && endpointBText.length > 0 + ToolTip.visible: endpointBHoverArea.containsMouse + && endpointBText.length > 0 + && endpointBLabel.implicitWidth > endpointBLabel.width + 1 ToolTip.text: endpointBText } Rectangle { diff --git a/src/ui/qml/components/PacketDetailsPane.qml b/src/ui/qml/components/PacketDetailsPane.qml index 64d9e82..540a554 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -446,7 +446,7 @@ Frame { font.family: monospace ? "Consolas" : "" font.pixelSize: 12 font.bold: bold - cursorVisible: activeFocus + cursorVisible: false } component SummaryFieldRow: Item { From 266e3ce7dd8b9d101fb4f33ab6083f196718f652 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 03:18:32 +0500 Subject: [PATCH 11/14] Update UI docs for compact flows workspace and endpoint presentation --- docs/features/Tauri_UI_spike.md | 10 ++++++++++ docs/ui/frontend_dto_mapping.md | 5 +++-- docs/ui/presentation_contract.md | 16 ++++++++++++---- docs/ui/tauri_qt_parity_audit.md | 6 +++--- experimental/tauri-ui-spike/README.md | 11 ++++++++++- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/features/Tauri_UI_spike.md b/docs/features/Tauri_UI_spike.md index 3ad93b7..8ed95f9 100644 --- a/docs/features/Tauri_UI_spike.md +++ b/docs/features/Tauri_UI_spike.md @@ -113,9 +113,18 @@ The `Flows` tab now supports: - separate checked-flow selection state for batch-oriented workflows - user-facing 1-based flow numbering while keeping stable backend `flow_index` - address family and fragmentation state from shared flow DTOs +- compact visible `Endpoint A` / `Endpoint B` columns in the flow table instead of separate address/port columns +- endpoint formatting aligned with Qt: + - IPv4 with port: `address : port` + - IPv4 without port: `address` + - IPv6 with port: `[address] : port` + - IPv6 without port: `address` + - missing/zero/invalid port: address only +- endpoint address/port are treated as key identifiers and should stay visible in the table rather than relying on tooltip-only display - conservative shared Wireshark display filter text plus copy - selected-flow packet loading over the existing backend `offset / limit` API with bounded append-only `Load More` - the initial selected-flow packet batch is intentionally small and bounded for responsiveness +- the lower selected-flow `Packets` / `Stream` controls, packet-count status, and `Load More` action now sit in one compact toolbar-style row - packet list columns now align more closely with Qt: - `#` - `Direction` @@ -132,6 +141,7 @@ The `Flows` tab now supports: - `Payload` - `Protocol` - the `Summary` tab now follows Qt more closely with a compact text-style packet summary block instead of metadata cards +- the top-shell `Open Capture...` action now uses a lighter desktop-style treatment closer to the Qt shell instead of a heavy filled primary button - Raw/Payload tabs now show the full available selected-packet byte text on demand rather than a preview-only display - Packet Details and Stream Item Details mode selectors now use compact tab styling instead of button styling - byte-backed packet details can recover after a valid source-capture attach diff --git a/docs/ui/frontend_dto_mapping.md b/docs/ui/frontend_dto_mapping.md index 706a2dd..5ee555c 100644 --- a/docs/ui/frontend_dto_mapping.md +++ b/docs/ui/frontend_dto_mapping.md @@ -115,7 +115,7 @@ For the current repository-level protocol support matrix and known protocol limi | port A | `FlowRow.port_a`; `FlowListModel.PortARole` | `FrontendFlowDto.port_a` | `FlowDto.port_a`; used for display/filter/Wireshark generation | aligned | frontend-neutral DTO | High | | address B | `FlowRow.address_b`; `FlowListModel.AddressBRole` | `FrontendFlowDto.address_b` | `FlowDto.address_b`; used for filter and Wireshark generation | aligned | frontend-neutral DTO | High | | port B | `FlowRow.port_b`; `FlowListModel.PortBRole` | `FrontendFlowDto.port_b` | `FlowDto.port_b`; used for display/filter/Wireshark generation | aligned | frontend-neutral DTO | High | -| combined endpoint text | `FlowRow.endpoint_a`, `endpoint_b`; `FlowListModel` filter uses them | `FrontendFlowDto.endpoint_a`, `endpoint_b` | `FlowDto.endpoint_a`, `endpoint_b`; used in Tauri filter but not shown in table | aligned at data level, not in visible columns | frontend-neutral DTO | Medium | +| combined endpoint text | `FlowRow.endpoint_a`, `endpoint_b`; `FlowListModel` filter uses them and Qt now renders them directly in compact `Endpoint A` / `Endpoint B` columns | `FrontendFlowDto.endpoint_a`, `endpoint_b` | `FlowDto.endpoint_a`, `endpoint_b`; Tauri now also renders endpoint-style columns and still reuses these fields for filtering | aligned at data and visible-column level; frontend formatting remains responsible for the final endpoint string shape | frontend-neutral DTO | Improved | | fragmentation indicator/count | `has_fragmented_packets`, `fragmented_packet_count`; `Frag` column in Qt | `FrontendFlowDto.has_fragmented_packets`, `fragmented_packet_count` | `FlowDto.has_fragmented_packets`, `fragmented_packet_count`; now surfaced as compact `Frag` marker text in Tauri | aligned enough for compact table | frontend-neutral DTO | Resolved | | packet count | `FlowRow.packet_count`; `FlowListModel.PacketsRole` | `FrontendFlowDto.packet_count` | `FlowDto.packet_count`; shown and filterable in Tauri | aligned | frontend-neutral DTO | High | | byte count | `FlowRow.total_bytes`; `FlowListModel.BytesRole` | `FrontendFlowDto.total_bytes` | `FlowDto.total_bytes`; shown and filterable in Tauri | aligned | frontend-neutral DTO | High | @@ -138,7 +138,7 @@ For the current repository-level protocol support matrix and known protocol limi | TCP flags | `PacketRow.tcp_flags_text`; `TcpFlagsTextRole` | `FrontendPacketDto.tcp_flags_text` | `PacketDto.tcp_flags_text` | aligned | frontend-neutral DTO | High | | IP fragmentation marker | `PacketRow.is_ip_fragmented`; `IsIpFragmentedRole` | `FrontendPacketDto.is_ip_fragmented` | `PacketDto.is_ip_fragmented`; now shown in a compact Tauri marker column | aligned enough for current scope | frontend-neutral DTO | Resolved | | suspected retransmission marker | `PacketRow.suspected_tcp_retransmission`; `SuspectedTcpRetransmissionRole`; `hasVisibleMarkers` in Qt model | `FrontendPacketDto.suspected_tcp_retransmission`; adapter derives via `suspected_tcp_retransmission_packet_indices(...)` | `PacketDto.suspected_tcp_retransmission`; now shown in a compact Tauri marker column | aligned enough for current scope | frontend-neutral DTO already sufficient | Resolved | -| packet pagination / offset / limit / total | Qt controller exposes `loadedPacketRowCount`, `totalPacketRowCount`, `canLoadMorePackets`; load-more semantics | `FrontendSelectedFlowPacketsResult.offset`, `limit`, `total_count`; adapter uses offset/limit query | Tauri `SelectedFlowPacketsDto` and `main.js` page state with `packetOffset`, fixed page size, prev/next | semantic mismatch: Qt load-more vs Tauri page stepping | app/session facts + frontend controller/model | High | +| packet pagination / offset / limit / total | Qt controller exposes `loadedPacketRowCount`, `totalPacketRowCount`, `canLoadMorePackets`; load-more semantics | `FrontendSelectedFlowPacketsResult.offset`, `limit`, `total_count`; adapter uses offset/limit query | Tauri `SelectedFlowPacketsDto` and `main.js` now use the same bounded append-only `Load More` shape with visible `Showing N of Total packets` status | the remaining gap is presentation density, not pagination semantics | app/session facts + frontend controller/model | Improved | | packet loading state | `MainController.packetsLoading` | none explicit in result DTO | Tauri `packetState = idle/loading/loaded/error` | shell/controller-state mismatch, not DTO gap | frontend controller/model | Medium | | packet error state | Qt largely controller-driven; packet list can be cleared/reset | result DTO has no packet-list `error_text` field | Tauri uses invoke exception path and local `packetErrorText` | no explicit packet-list error DTO | frontend controller/model or future list-state DTO | Low | | packet unavailable state | Qt uses source-availability and selected-flow state | no explicit packet-list unavailable text field | Tauri currently infers from shell/open/selection state | acceptable for now | app/session facts + frontend controller/model | Low | @@ -150,6 +150,7 @@ For the current repository-level protocol support matrix and known protocol limi | details title / header fields | `PacketDetailsViewModel.detailsTitle`, `headerPrimaryText`, `headerSecondaryText`, `badgeText` | `FrontendPacketDetailsDto.details_title` now carries the shared packet-details title; header/badge fields are still absent | Tauri now consumes shared `details_title`; summary labels remain local | title is partially aligned, header/badge remain Qt-specific today | frontend-neutral DTO for shared title, deferred for header/badge | Improved | | summary text | `PacketDetailsViewModel.summaryText`; built in `MainController::buildPacketSummary(...)` | `FrontendPacketDetailsDto.summary_text` still carries the legacy shared text summary block built from existing packet/session facts | Qt and Tauri now keep this text as a fallback when structured layered summary data is absent | fallback path remains intentionally text-first for unavailable/older cases | frontend-neutral DTO fallback | Improved | | structured summary layers | Qt previously kept summary mostly text-first with local formatting | `FrontendPacketDetailsDto.summary_layers` now carries a shared layered packet-summary tree with generic `layer -> fields -> children` structure | Qt Summary and Tauri Summary now both render collapsible shared layers first and fall back to `summary_text` only when layers are unavailable | shared layer model now covers Frame/Ethernet/VLAN/IPv4/IPv6/TCP/UDP/ARP plus a conservative final higher-level layer for already-recognized TLS/QUIC/DNS/HTTP/ICMP/ICMPv6 packets; base layers now include file/flow packet numbering, Ethernet MACs, and conservative IPv4/IPv6 header fields derived only during selected-packet/on-demand decoding | frontend-neutral DTO | Improved | +| Qt Summary text selection | `PacketDetailsPane.qml` now renders Summary layer titles/fields/warnings through read-only selectable text controls | no DTO change; behavior is purely frontend presentation over existing summary-layer / text fields | not applicable in current Tauri shell | this PR changes usability only; backend/session contracts are unchanged | Qt frontend presentation only | Improved | | raw preview text | `PacketDetailsViewModel.hexText` | `FrontendPacketDetailsDto.raw_preview_text` | `PacketDetailsDto.raw_preview_text` | aligned | frontend-neutral DTO | High | | raw preview truncated metadata | Qt text and UI state imply it, but not clearly as a standalone property | `FrontendPacketDetailsDto.raw_preview_truncated`, `raw_preview_available`, `raw_preview_unavailable_text` | Tauri uses them explicitly | Tauri/frontend-neutral shape is cleaner than Qt VM surface here | frontend-neutral DTO | High | | payload preview text | `PacketDetailsViewModel.payloadText` | `FrontendPacketDetailsDto.payload_preview_text` | `PacketDetailsDto.payload_preview_text` | aligned | frontend-neutral DTO | High | diff --git a/docs/ui/presentation_contract.md b/docs/ui/presentation_contract.md index 7db08c4..e212db6 100644 --- a/docs/ui/presentation_contract.md +++ b/docs/ui/presentation_contract.md @@ -267,10 +267,8 @@ Each flow row should expose at least the following user-facing fields: - protocol; - protocol hint; - service; -- address A; -- port A; -- address B; -- port B; +- endpoint A; +- endpoint B; - fragmentation indicator; - packet count; - byte count. @@ -279,6 +277,14 @@ Each flow row should expose at least the following user-facing fields: - Flow index is displayed as a 1-based row identifier tied to the session flow index. - Address family is shown as `IPv4` or `IPv6`. +- The visible flow table now uses compact `Endpoint A` / `Endpoint B` columns rather than separate address/port columns. +- Endpoint formatting rules are: + - IPv4 with port: `address : port`; + - IPv4 without port: `address`; + - IPv6 with port: `[address] : port`; + - IPv6 without port: `address`; + - missing/zero/invalid port: address only. +- Endpoint address/port are treated as key identifiers and should remain fully visible through adequate column width and horizontal scrolling rather than endpoint overlap as the normal display path. - Protocol hint is presentation-formatted: - `possible_tls` -> `Possible TLS`; - `possible_quic` -> `Possible QUIC`; @@ -423,6 +429,7 @@ Shared expectations: - all packets loaded; - load more available. - load-more is tied to the selected flow only. +- frontends may present the lower selected-flow `Packets` / `Stream` controls as one compact toolbar-style row as long as packet/stream switching, packet-count status, and `Load More` remain visible and consistent. ### Selected packet behavior @@ -491,6 +498,7 @@ Qt currently uses formatted summary text rather than a purely structured field g Current direction note: - packet details Summary now has a first shared structured decoded-layer list for selected-packet/on-demand rendering; +- Qt Summary text inside the structured inspector is selectable/copyable via read-only text controls; this is presentation-only and does not change packet/session semantics; - the current narrow layer model covers already-decoded facts such as Frame, Ethernet, VLAN, MPLS, ARP, IGMP, IPv4, IPv6, TCP, and UDP; - the Frame layer should show packet index in file and, when selected-flow context is available, packet index within the selected flow; - the Ethernet layer should expose source/destination MAC addresses and decoded EtherType text; diff --git a/docs/ui/tauri_qt_parity_audit.md b/docs/ui/tauri_qt_parity_audit.md index 3d2f151..382ae50 100644 --- a/docs/ui/tauri_qt_parity_audit.md +++ b/docs/ui/tauri_qt_parity_audit.md @@ -48,10 +48,10 @@ This audit is based on static inspection of: | Export unselected flows | Qt exports the inverse of checked flows over the loaded set when source bytes are available. | Same inverse-of-checked-flow export is implemented. | Low gap. | Low | Keep as-is. | | Smart Export | Qt has a richer dialog plus explicit progress/cancel UI during long-running export. | Tauri mirrors scope/base/output options and folder mode, but uses a simpler custom dialog and lighter status reporting. | Missing richer Qt progress/cancel UX parity and some dialog-level copy/layout fidelity. | Medium | Small UI polish pass: align labels/help text first, then consider richer progress/cancel parity. | | Settings | Qt `View -> Settings` is implemented and currently exposes the shared runtime-safe settings slice directly through QML/controller properties. | Tauri `View -> Settings` supports the same currently wired settings slice. | Core working slice matches, but persistence remains deferred and the dialog is still lighter-weight. | Medium | Keep settings scope stable; defer persistence until broader settings strategy is agreed. | -| Flows table | Qt uses a dense list-based table with checked-flow boxes, sort buttons, filter, Wireshark filter row, and `Send flow to Analysis`. | Tauri has filtering, sorting, checked-flow state, Wireshark filter row toggle, and frontend virtualization/windowing. | Tauri is closer functionally, but row styling, per-column density, and exact workflow affordances still differ. | Medium | UI polish pass focused on row density, spacing, and top-of-flows controls. | +| Flows table | Qt uses a dense list-based table with checked-flow boxes, sort buttons, filter, Wireshark filter row, `Send flow to Analysis`, compact `Endpoint A` / `Endpoint B` columns, and horizontal scrolling for wide endpoint-heavy layouts. | Tauri has filtering, sorting, checked-flow state, Wireshark filter row toggle, frontend virtualization/windowing, and matching endpoint-style columns with the same address/port formatting rules. | Tauri is closer functionally; the remaining gap is mostly row styling and compactness polish rather than column semantics. | Medium | UI polish pass focused on row density, spacing, and top-of-flows controls. | | Checked-flow selection and selection status | Qt shows checked-flow state in-table and a bottom selection status bar when any flows are checked. | Tauri keeps checked-flow state across sorting/filtering and shows a compact checked-flow status bar. | Small presentation gap only. | Low | Keep behavior; only match wording/styling if needed. | -| Packet list | Qt packet list is bounded, supports `Load more`, and emphasizes direction/flags with compact visual treatment plus packet markers such as `Suspected retransmission`. | Tauri packet list is bounded with append-only `Load More`, Qt-like visible columns, direction chips, TCP flag highlighting, and shared packet marker display including suspected retransmission. | Main remaining gap is row-density and lower-workspace polish rather than column semantics. | Medium | Keep the bounded `Load More` model and continue with compact row styling and lower-workspace visual polish. | -| Packet details | Qt packet/stream details pane is richer: warnings block extraction, dynamic header for stream items, better text panes, and tighter tab behavior. | Tauri supports `Summary / Raw / Payload / Protocol`, full on-demand Raw/Payload text for the selected packet, checksum setting, and now consumes the same shared layered packet-summary tree as Qt for the Summary tab, with text fallback when layers are unavailable. | The main gap is now inspector polish and deeper protocol-specific presentation, not the lack of a shared Summary structure. | Medium | Keep packet-details follow-ups focused on expanding structured decoded-layer presentation carefully rather than broad workflow changes. | +| Packet list | Qt packet list is bounded, supports `Load more`, and emphasizes direction/flags with compact visual treatment plus packet markers such as `Suspected retransmission`. The lower `Packets` / `Stream` controls now sit in one compact toolbar row with status text and `Load More`. | Tauri packet list is bounded with append-only `Load More`, Qt-like visible columns, direction chips, TCP flag highlighting, shared packet marker display including suspected retransmission, and the same compact lower toolbar-style row. | Main remaining gap is row-density and lower-workspace polish rather than column semantics. | Medium | Keep the bounded `Load More` model and continue with compact row styling and lower-workspace visual polish. | +| Packet details | Qt packet/stream details pane is richer: warnings block extraction, dynamic header for stream items, better text panes, tighter tab behavior, and selectable/copyable Summary text in the Qt inspector. | Tauri supports `Summary / Raw / Payload / Protocol`, full on-demand Raw/Payload text for the selected packet, checksum setting, and now consumes the same shared layered packet-summary tree as Qt for the Summary tab, with text fallback when layers are unavailable. | The main gap is now inspector polish and deeper protocol-specific presentation, not the lack of a shared Summary structure. Qt currently retains the better copy/select story in the inspector. | Medium | Keep packet-details follow-ups focused on expanding structured decoded-layer presentation carefully rather than broad workflow changes. | | Stream view | Qt stream view is selected-flow-only, bounded, lazy, and uses directional bubble/card presentation plus constricted badges and `Load more`. | Tauri stream view is now selected-flow-only, bounded, lazy, selectable, and uses directional cards with left/right alignment by direction. | Main remaining gap is detail richness and contextual polish, not the basic presentation model. | Medium | Keep backend loading semantics unchanged and continue with stream-item-detail polish only. | | Stream item details | Qt has a dedicated stream-item detail presentation through the packet-details pane, with contextual headers, summary text, payload/protocol tabs, and source-packet summaries. | Tauri now shows a compact header block plus `Summary / Payload / Protocol` tabs driven by shared stream-item DTO fields and bounded fallback text. | The major workflow gap is closed; remaining differences are mostly protocol-specific formatting and the lack of stream-to-packet navigation. | Medium | Keep future work narrow: protocol-specific formatting polish and optional stream-to-packet navigation only. | | Statistics overview | Qt uses summary cards plus denser percentage-heavy protocol/family tables and conditional top-talker sections. | Tauri provides overview cards, transport/family/protocol-hint summaries, QUIC/TLS blocks, and top endpoints/ports. | Capability is mostly present, but percentage formatting and some conditional presentation are still simpler. | Medium | Statistics polish pass focused on percentages, compactness, and drill-down affordances. | diff --git a/experimental/tauri-ui-spike/README.md b/experimental/tauri-ui-spike/README.md index 53ef86f..e1c5b8f 100644 --- a/experimental/tauri-ui-spike/README.md +++ b/experimental/tauri-ui-spike/README.md @@ -130,6 +130,14 @@ Implemented slice: - The Flows table also keeps a separate checked-flow selection state for future batch workflows without changing the active selected flow. - The flow table shows a user-facing 1-based flow number while keeping stable `flow_index` internally. - The flow table surfaces address family and fragmentation state from the shared flow DTO. +- The visible flow table uses compact `Endpoint A` / `Endpoint B` columns rather than separate address/port columns. +- Endpoint formatting follows the current shared UI rules: + - IPv4 with port: `address : port` + - IPv4 without port: `address` + - IPv6 with port: `[address] : port` + - IPv6 without port: `address` + - missing/zero/invalid port: address only +- Endpoint address/port are treated as key identifiers and should stay visible in the row rather than relying on tooltip-only display. - When one or more flow checkboxes are active, the Flows workspace shows a compact bottom status bar with the checked-flow count. - Opening a new path clears stale overview, flows, packets, stream, analysis, and prior errors before the next backend call. - Open controls are disabled while an open is in flight. @@ -137,12 +145,13 @@ Implemented slice: - Partial/truncated opens now surface a dedicated warning banner instead of only relying on generic shell status text. - Clicking a flow row loads that flow's packets and resets the bounded packet list to its initial batch. - Flow selection now updates loading state immediately and ignores stale packet/stream/analysis responses from older selections. -- The lower-left Flows workspace has `Packets` and `Stream` tabs. +- The lower-left Flows workspace keeps `Packets` and `Stream` as tabs, but they now sit in one compact toolbar row together with packet-count status and `Load More`. - The initial selected-flow packet batch is intentionally small and bounded for responsiveness. - If the current filter hides the selected flow, the shell clears visible flow/packet/stream/details state to avoid stale UI. - Clicking a packet row loads packet details and full available Raw/Payload byte text for the selected packet when byte-backed inspection is available. - Packet Details and Stream Item Details mode selectors now use compact tab styling instead of looking like standalone buttons. - The selected-packet inspector consumes shared packet-details DTO fields for the panel title, protocol-specific payload tab title, and explicit no-payload state. +- The top-shell `Open Capture...` action now uses a lighter desktop-style treatment closer to the Qt shell instead of a heavy filled primary button. - The Stream tab keeps stream reconstruction bounded to the selected flow plus the current packet/item budgets. - Stream items are rendered as directional cards rather than a table and now drive a richer Selected Stream Item Details view with a compact header plus `Summary / Payload / Protocol` tabs. - Selecting a stream item does not yet navigate to packet details or source packets. From c6fcf0c7c3108023f4708f42eeea1ca1a138833f Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 03:30:34 +0500 Subject: [PATCH 12/14] Add Tauri section aria labels and decouple Qt flow gutter from scrollbar visibility --- experimental/tauri-ui-spike/web/index.html | 4 ++-- src/ui/qml/components/FlowTable.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/tauri-ui-spike/web/index.html b/experimental/tauri-ui-spike/web/index.html index 8b38f5f..220306d 100644 --- a/experimental/tauri-ui-spike/web/index.html +++ b/experimental/tauri-ui-spike/web/index.html @@ -108,7 +108,7 @@
-
+

No capture loaded.

@@ -168,7 +168,7 @@ >
-
+
diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index a859a4a..5a59107 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -266,7 +266,7 @@ Frame { ListView { id: flowListView - readonly property int rightGutter: flowScrollBar.visible ? flowScrollBar.width + 10 : 0 + readonly property int rightGutter: Math.max(flowScrollBar.implicitWidth, 12) + 10 anchors.fill: parent anchors.margins: 1 From ff2ad6890efcfb0ca5c4227917b70bab7f228a54 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 13:04:39 +0500 Subject: [PATCH 13/14] Finalize Qt flow-table wheel forwarding and packet-details title tooltip cleanup --- src/ui/qml/components/FlowTable.qml | 42 ++++++++++++++++++++- src/ui/qml/components/FlowWorkspacePane.qml | 1 - src/ui/qml/components/PacketDetailsPane.qml | 20 ++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index 5a59107..e9039e5 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -114,6 +114,25 @@ Frame { : displayAddress } + function maxHorizontalOffset() { + return Math.max(0, flowTableScroller.contentWidth - flowTableScroller.width) + } + + function scrollHorizontally(delta) { + const maxOffset = root.maxHorizontalOffset() + if (maxOffset <= 0 || delta === 0) { + return false + } + + const nextX = Math.max(0, Math.min(flowTableScroller.contentX - delta, maxOffset)) + if (Math.abs(nextX - flowTableScroller.contentX) < 0.5) { + return false + } + + flowTableScroller.contentX = nextX + return true + } + background: Rectangle { color: "#ffffff" border.color: "#d8dee9" @@ -203,7 +222,7 @@ Frame { id: flowTableViewport Layout.fillWidth: true Layout.fillHeight: true - + Flickable { id: flowTableScroller anchors.fill: parent @@ -219,6 +238,27 @@ Frame { } } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + + onWheel: function(wheel) { + const horizontalDelta = wheel.pixelDelta.x !== 0 + ? wheel.pixelDelta.x + : wheel.angleDelta.x !== 0 + ? wheel.angleDelta.x / 8 + : (wheel.modifiers & Qt.ShiftModifier) && wheel.angleDelta.y !== 0 + ? wheel.angleDelta.y / 8 + : 0 + + if (horizontalDelta !== 0 && root.scrollHorizontally(horizontalDelta)) { + wheel.accepted = true + } else { + wheel.accepted = false + } + } + } + Item { anchors.fill: parent diff --git a/src/ui/qml/components/FlowWorkspacePane.qml b/src/ui/qml/components/FlowWorkspacePane.qml index d5f97d7..7d72eea 100644 --- a/src/ui/qml/components/FlowWorkspacePane.qml +++ b/src/ui/qml/components/FlowWorkspacePane.qml @@ -256,7 +256,6 @@ Item { currentIndex: flowDetailTabs.currentIndex PacketList { - titleText: root.unrecognizedPacketsSelected ? "Unrecognized Packets" : "Packets" emptyText: root.unrecognizedPacketsSelected ? "Select the unrecognized packets list to inspect packets" : "Select a flow to inspect packets" diff --git a/src/ui/qml/components/PacketDetailsPane.qml b/src/ui/qml/components/PacketDetailsPane.qml index 540a554..6c7ccb1 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -558,11 +558,25 @@ Frame { } } - SelectableText { + Item { Layout.fillWidth: true - text: summaryLayerCard.titleText - textColor: "#0f172a" + implicitHeight: titleTextItem.implicitHeight clip: true + + SelectableText { + id: titleTextItem + anchors.fill: parent + text: summaryLayerCard.titleText + textColor: "#0f172a" + clip: true + } + + HoverHandler { + id: titleHoverHandler + } + + ToolTip.visible: titleHoverHandler.hovered && titleTextItem.contentWidth > titleTextItem.width + 1 + ToolTip.text: summaryLayerCard.titleText } Rectangle { From c9ac8d87868597657c9e3b490b0460fa03461ed2 Mon Sep 17 00:00:00 2001 From: AlexeyVasilev Date: Thu, 2 Jul 2026 13:18:09 +0500 Subject: [PATCH 14/14] Fix final QML review nits for endpoint tooltips and selectable text clipping --- src/ui/qml/components/FlowTable.qml | 3 ++- src/ui/qml/components/PacketDetailsPane.qml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ui/qml/components/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index e9039e5..f32986e 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -18,6 +18,7 @@ Frame { readonly property int tableContentLeftMargin: 6 readonly property int tableContentRightMargin: 6 readonly property int selectionColumnWidth: 42 + readonly property int rowClickLeftMargin: root.tableContentLeftMargin + root.selectionColumnWidth + root.tableRowSpacing - 2 readonly property int indexColumnWidth: 64 readonly property int familyColumnWidth: 74 readonly property int protocolColumnWidth: 86 @@ -540,7 +541,7 @@ Frame { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.leftMargin: 52 + anchors.leftMargin: root.rowClickLeftMargin hoverEnabled: true onClicked: { flowListView.currentIndex = index diff --git a/src/ui/qml/components/PacketDetailsPane.qml b/src/ui/qml/components/PacketDetailsPane.qml index 6c7ccb1..2be326a 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -436,12 +436,14 @@ Frame { property bool monospace: false property bool bold: false property int textWrapMode: TextEdit.NoWrap + property bool clipOverflow: textWrapMode === TextEdit.NoWrap readOnly: true activeFocusOnTab: false selectByMouse: true textFormat: TextEdit.PlainText wrapMode: textWrapMode + clip: clipOverflow color: textColor font.family: monospace ? "Consolas" : "" font.pixelSize: 12