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. diff --git a/experimental/tauri-ui-spike/web/index.html b/experimental/tauri-ui-spike/web/index.html index e24a772..220306d 100644 --- a/experimental/tauri-ui-spike/web/index.html +++ b/experimental/tauri-ui-spike/web/index.html @@ -108,11 +108,10 @@
-
-
-
-

Flows

-

No capture loaded.

+
+
+
+

No capture loaded.

@@ -169,15 +168,14 @@

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..097a67e 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; @@ -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"), @@ -577,6 +576,59 @@ 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 = hasPort && trimmedAddress.includes(":") + ? `[${trimmedAddress}]` + : trimmedAddress; + + return hasPort + ? `${displayAddress} : ${numericPort}` + : 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); } @@ -1126,9 +1178,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": @@ -1996,10 +2048,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; @@ -2420,8 +2468,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} + ${renderEndpointCell(flow.address_a, flow.port_a)} + ${renderEndpointCell(flow.address_b, flow.port_b)} ${formatPlainInteger(flow.packet_count)} ${formatPlainInteger(flow.total_bytes)} @@ -3576,7 +3624,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 a04fdb1..dc258a7 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 { @@ -615,7 +628,7 @@ body.is-resizing-vertical { .workspace-shell { display: grid; grid-template-rows: auto 1fr; - gap: 8px; + gap: 6px; min-height: 0; } @@ -629,8 +642,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; @@ -763,21 +776,29 @@ body.is-resizing-vertical { .details-scroll { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; 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; 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 +806,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 +851,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; } @@ -962,20 +996,43 @@ select:disabled { flex-wrap: wrap; } +.flow-view-toolbar { + display: flex; + align-items: flex-end; + gap: 10px; + margin-bottom: 4px; + 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: 6px; + margin-bottom: -1px; flex: 0 0 auto; - padding: 0 2px; - border-bottom: 1px solid var(--border); + padding: 0; + border-bottom: none; } .subtab-button { min-width: 96px; - min-height: 34px; - padding: 7px 14px 8px; + min-height: 28px; + padding: 5px 12px 6px; border: 1px solid var(--border); border-bottom: none; border-radius: 10px 10px 0 0; @@ -1006,7 +1063,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 +1072,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; @@ -1042,6 +1099,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); @@ -1073,7 +1139,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; @@ -1129,6 +1195,39 @@ 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: max-content 1ch 5ch; + align-items: center; + column-gap: 0.35rem; + justify-content: start; +} + +.endpoint-address { + 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; } @@ -1229,7 +1328,7 @@ select:disabled { } .flow-table-wrap .data-table tbody tr.flow-row td { - height: 36px; + height: 32px; vertical-align: middle; white-space: nowrap; } @@ -1277,8 +1376,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 +1957,7 @@ select:disabled { display: flex; flex: 1 1 auto; flex-direction: column; - gap: 6px; + gap: 2px; min-height: 0; } @@ -1871,7 +1970,7 @@ select:disabled { display: flex; flex: 1 1 auto; flex-direction: column; - gap: 6px; + gap: 4px; min-height: 0; } @@ -1888,7 +1987,7 @@ select:disabled { .packet-summary-layers { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } .packet-summary-layer { @@ -1907,11 +2006,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 +2056,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 +2093,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 +2143,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 +2166,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 +2239,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 +2274,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/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/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/FlowTable.qml b/src/ui/qml/components/FlowTable.qml index 792aa3d..f32986e 100644 --- a/src/ui/qml/components/FlowTable.qml +++ b/src/ui/qml/components/FlowTable.qml @@ -14,20 +14,37 @@ 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 rowClickLeftMargin: root.tableContentLeftMargin + root.selectionColumnWidth + root.tableRowSpacing - 2 readonly property int indexColumnWidth: 64 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: 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) @@ -80,12 +97,55 @@ 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 = hasPort && trimmedAddress.indexOf(":") >= 0 + ? "[" + trimmedAddress + "]" + : trimmedAddress + + return hasPort + ? displayAddress + " : " + numericPort + : 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" radius: 8 } + TextMetrics { + id: endpointTextMetrics + font.family: "Consolas" + text: "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff] : 65535" + } + onFlowModelChanged: syncSelectedFlowRow() onSelectedFlowIndexChanged: syncSelectedFlowRow() onVisibleChanged: { @@ -96,13 +156,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 - - Label { - text: "Flows" - font.pixelSize: 18 - font.bold: true - } + spacing: 6 RowLayout { Layout.fillWidth: true @@ -165,283 +219,346 @@ Frame { } } - Rectangle { - Layout.fillWidth: true - height: 1 - color: "#e2e8f0" - } - - 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: "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: "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 { + Item { + id: flowTableViewport 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 + Flickable { + id: flowTableScroller 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 + 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 } + } - 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 - - onFlowCheckedChanged: { - if (selectionCheckBox.checked !== flowChecked) { - selectionCheckBox.checked = flowChecked - } + 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 } + } + } - width: flowListView.width - height: 36 - color: selected - ? "#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 { + anchors.fill: parent - Text { - text: flowIndex + 1 - Layout.preferredWidth: root.indexColumnWidth - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - } - Text { - text: family - Layout.preferredWidth: root.familyColumnWidth - verticalAlignment: Text.AlignVCenter + Item { + id: flowHeaderContainer + width: parent.width + height: flowHeaderRow.implicitHeight + clip: true + + 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) } } - Text { - text: protocol - Layout.preferredWidth: root.protocolColumnWidth - verticalAlignment: Text.AlignVCenter - } - Item { - Layout.preferredWidth: root.protocolHintColumnWidth - implicitHeight: protocolHintLabel.implicitHeight - - Label { - id: protocolHintLabel - anchors.fill: parent - text: protocolHint - elide: Text.ElideRight - 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.fillWidth: true - Layout.preferredWidth: root.serviceColumnWidth - implicitHeight: serviceHintLabel.implicitHeight - - Label { - id: serviceHintLabel - anchors.fill: parent - text: serviceHint - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } + Rectangle { + id: flowBodyContainer + y: flowHeaderContainer.height + width: parent.width + height: Math.max(0, parent.height - flowHeaderContainer.height - (flowTableHorizontalScrollBar.visible ? flowTableHorizontalScrollBar.height : 0)) + color: "#f8fafc" + border.color: "#e2e8f0" + radius: 6 - MouseArea { - id: serviceHintHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true - } + ListView { + id: flowListView + readonly property int rightGutter: Math.max(flowScrollBar.implicitWidth, 12) + 10 - ToolTip.visible: serviceHintHoverArea.containsMouse && serviceHintLabel.truncated - ToolTip.text: serviceHintLabel.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.addressColumnWidth - implicitHeight: addressALabel.implicitHeight - - Label { - id: addressALabel - anchors.fill: parent - text: addressA - elide: Text.ElideMiddle - verticalAlignment: Text.AlignVCenter + 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 + } } - MouseArea { - id: addressAHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton - hoverEnabled: true - } + width: flowListView.width + height: 32 + clip: true + color: selected + ? "#dbeafe" + : (index % 2 === 0 ? "#ffffff" : "#f8fafc") + + 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) + } + } + } + } - ToolTip.visible: addressAHoverArea.containsMouse && addressALabel.truncated - ToolTip.text: addressALabel.text - } - Text { - text: portA - Layout.preferredWidth: root.portColumnWidth - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - } - Item { - Layout.fillWidth: true - Layout.preferredWidth: root.addressColumnWidth - implicitHeight: addressBLabel.implicitHeight - - Label { - id: addressBLabel - anchors.fill: parent - text: addressB - elide: Text.ElideMiddle - verticalAlignment: Text.AlignVCenter + 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 + } + + 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 + } + + MouseArea { + id: serviceHintHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } + + 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: endpointAHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } + + ToolTip.visible: endpointAHoverArea.containsMouse + && endpointAText.length > 0 + && endpointALabel.implicitWidth > endpointALabel.width + 1 + 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 + } + + MouseArea { + id: endpointBHoverArea + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + } + + ToolTip.visible: endpointBHoverArea.containsMouse + && endpointBText.length > 0 + && endpointBLabel.implicitWidth > endpointBLabel.width + 1 + 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 { - id: addressBHoverArea - anchors.fill: parent - acceptedButtons: Qt.NoButton + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.rowClickLeftMargin hoverEnabled: true + onClicked: { + flowListView.currentIndex = index + root.flowSelected(flowIndex) + } } - - ToolTip.visible: addressBHoverArea.containsMouse && addressBLabel.truncated - ToolTip.text: addressBLabel.text - } - Text { - text: portB - Layout.preferredWidth: root.portColumnWidth - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - } - Rectangle { - Layout.preferredWidth: root.fragColumnWidth - implicitHeight: 24 - 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 { @@ -451,7 +568,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..7d72eea 100644 --- a/src/ui/qml/components/FlowWorkspacePane.qml +++ b/src/ui/qml/components/FlowWorkspacePane.qml @@ -33,6 +33,53 @@ 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 lowerToolbarStatusColor() { + if (!root.packetsTabSelected && !root.sourceCaptureAvailable && root.selectedFlowIndex >= 0) { + return "#8a6a12" + } + + return "#6b7280" + } + + 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 +150,102 @@ Item { ColumnLayout { anchors.fill: parent - spacing: 8 + 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: 28 + implicitWidth: 108 - TabButton { - text: "Packets" - implicitHeight: 34 - - 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: 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" + } + + background: Rectangle { + radius: 6 + color: parent.checked + ? "#ffffff" + : parent.hovered + ? "#f8fafc" + : "#f1f5f9" + border.color: parent.checked ? "#cbd5e1" : "#e2e8f0" + } } } - TabButton { - text: "Stream" - implicitHeight: 34 - 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" - } + Label { + Layout.fillWidth: true + text: root.lowerToolbarStatusText() + color: root.lowerToolbarStatusColor() + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: 12 + } - background: Rectangle { - radius: 6 - color: parent.checked - ? "#ffffff" - : parent.hovered - ? "#f8fafc" - : "#f1f5f9" - border.color: parent.checked ? "#cbd5e1" : "#e2e8f0" + 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() + } } } } @@ -178,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" @@ -189,6 +266,7 @@ Item { loadedPacketRowCount: root.loadedPacketRowCount totalPacketRowCount: root.totalPacketRowCount canLoadMorePackets: root.canLoadMorePackets + showToolbar: false onPacketSelected: function(packetIndex) { root.packetSelected(packetIndex) } @@ -209,6 +287,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 e75fd84..2be326a 100644 --- a/src/ui/qml/components/PacketDetailsPane.qml +++ b/src/ui/qml/components/PacketDetailsPane.qml @@ -431,6 +431,26 @@ Frame { } } + component SelectableText: TextEdit { + property color textColor: "#0f172a" + 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 + font.bold: bold + cursorVisible: false + } + component SummaryFieldRow: Item { required property var modelData readonly property string labelText: modelData && modelData["label"] !== undefined && modelData["label"] !== null @@ -447,23 +467,20 @@ Frame { id: rowLayout anchors.fill: parent columns: fullWidth ? 1 : 2 - columnSpacing: 8 - rowSpacing: 2 + 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 } } } @@ -502,13 +519,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 +544,8 @@ Frame { ) } padding: 0 - implicitWidth: 18 - implicitHeight: 18 + implicitWidth: 16 + implicitHeight: 16 contentItem: Label { text: parent.text @@ -543,12 +560,25 @@ Frame { } } - Label { + Item { Layout.fillWidth: true - text: summaryLayerCard.titleText - font.pixelSize: 13 - font.bold: false - color: "#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 { @@ -573,7 +603,7 @@ Frame { ColumnLayout { Layout.fillWidth: true visible: summaryLayerCard.expanded - spacing: 5 + spacing: 4 Repeater { model: summaryLayerCard.fieldRows @@ -621,7 +651,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 6 + spacing: 4 Label { text: root.detailsTitle() @@ -656,7 +686,7 @@ Frame { Label { Layout.fillWidth: true text: root.headerPrimaryText() - font.pixelSize: 15 + font.pixelSize: 14 font.bold: true color: "#0f172a" elide: Text.ElideRight @@ -695,7 +725,7 @@ Frame { id: packetTabs Layout.fillWidth: true visible: !root.isStreamItemDetails() - spacing: 6 + spacing: 4 background: Rectangle { color: "transparent" @@ -703,13 +733,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" } @@ -727,13 +757,13 @@ Frame { TabButton { text: "Raw" - implicitHeight: 34 + implicitHeight: 28 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 +781,13 @@ Frame { TabButton { text: root.payloadTabTitle() - implicitHeight: 34 + implicitHeight: 28 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 +805,13 @@ Frame { TabButton { text: "Protocol" - implicitHeight: 34 + implicitHeight: 28 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" } @@ -802,7 +832,7 @@ Frame { id: streamTabs Layout.fillWidth: true visible: root.isStreamItemDetails() - spacing: 6 + spacing: 4 background: Rectangle { color: "transparent" @@ -810,13 +840,13 @@ Frame { TabButton { text: "Summary" - implicitHeight: 34 + implicitHeight: 28 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 +864,13 @@ Frame { TabButton { text: root.payloadTabTitle() - implicitHeight: 34 + implicitHeight: 28 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 +888,13 @@ Frame { TabButton { text: "Protocol" - implicitHeight: 34 + implicitHeight: 28 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 +929,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 + spacing: 4 Rectangle { Layout.fillWidth: true @@ -907,16 +937,16 @@ Frame { color: "#fff6d6" border.color: "#e7d38d" radius: 6 - implicitHeight: warningLabel.implicitHeight + 16 + implicitHeight: warningLabel.implicitHeight + 12 - Text { + SelectableText { id: warningLabel anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: 7 - wrapMode: Text.Wrap - color: "#7a5d10" + anchors.margins: 6 + textColor: "#7a5d10" + textWrapMode: TextEdit.Wrap text: packetSummaryPane.warningText.length > 0 ? "Warnings\n" + packetSummaryPane.warningText : "" @@ -933,7 +963,7 @@ Frame { ColumnLayout { width: parent.width - spacing: 8 + spacing: 6 Repeater { model: packetSummaryPane.layers @@ -998,7 +1028,7 @@ Frame { ColumnLayout { anchors.fill: parent - spacing: 8 + spacing: 6 Rectangle { Layout.fillWidth: true @@ -1006,16 +1036,16 @@ Frame { color: "#fff6d6" border.color: "#e7d38d" radius: 6 - implicitHeight: streamWarningLabel.implicitHeight + 16 + implicitHeight: streamWarningLabel.implicitHeight + 12 - Text { + SelectableText { id: streamWarningLabel anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: 7 - wrapMode: Text.Wrap - color: "#7a5d10" + anchors.margins: 6 + textColor: "#7a5d10" + textWrapMode: TextEdit.Wrap text: parent.parent.parent.warningText.length > 0 ? "Warnings\n" + parent.parent.parent.warningText : "" diff --git a/src/ui/qml/components/PacketList.qml b/src/ui/qml/components/PacketList.qml index 7bdb612..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" @@ -164,23 +165,11 @@ 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 - visible: root.packetsLoading || root.totalPacketRowCount > 0 + visible: root.showToolbar && (root.packetsLoading || root.totalPacketRowCount > 0) spacing: 8 Label { @@ -203,7 +192,7 @@ Frame { RowLayout { Layout.fillWidth: true - spacing: 10 + spacing: 8 Label { text: "#" @@ -293,7 +282,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 +300,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 +327,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 +352,7 @@ Frame { Rectangle { Layout.fillWidth: true - implicitHeight: 24 + implicitHeight: 20 radius: 4 color: root.unrecognizedMode ? "transparent" @@ -398,7 +387,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..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" @@ -67,27 +68,15 @@ 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 - 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 { @@ -143,9 +132,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 +163,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 +174,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 +186,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 +216,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 +231,7 @@ Frame { color: "#475569" elide: Text.ElideRight verticalAlignment: Text.AlignVCenter - font.pixelSize: 12 + font.pixelSize: 11 } MouseArea {