git.iko.soy/parseable-macos
on master help sign in
Add column header right-click filtering for query and live tail views (#16)

Right-clicking a column header now shows "Filter by Value" and "Exclude Value" submenus listing the distinct values present in that column. Selecting a value applies the same filter mechanism already used by cell-level right-click filtering: SQL WHERE clause modification in the query view, and in-memory column filters in the live tail view. Values are capped at 20 entries to keep the menu manageable, with a count of remaining values shown when truncated. https://claude.ai/code/session_01QCrUbgWJov4DZSkUk7cZud Co-authored-by: Claude <noreply@anthropic.com>

commita99b1f3ef52f9f4afe159694cbe0eb769da7bb4f authoriko <mail@iko.soy> dateSun 15 Feb 2026, 14:11 (8 months ago) parente7b06dd
2 files changed+61−1
ParseableViewer/Views/LiveTailView.swift+3 ParseableViewer/Views/LogTableView.swift+58−1
ParseableViewer/Views/LiveTailView.swift+3
@@ -301,6 +301,9 @@ struct LiveTailView: View {
301301 records: viewModel.filteredEntries.map { $0.record },
302302 onMoveColumn: { from, to in
303303 viewModel.moveColumn(from, to: to)
304+ },
305+ onColumnFilter: { column, value, exclude in
306+ viewModel.addColumnFilter(column: column, value: value, exclude: exclude)
304307 }
305308 )
306309 }
ParseableViewer/Views/LogTableView.swift+58−1
@@ -98,7 +98,8 @@ struct LogTableView: View {
9898 sortAscending: $sortAscending,
9999 columnWidths: $columnWidths,
100100 records: records,
101− onMoveColumn: onMoveColumn
101+ onMoveColumn: onMoveColumn,
102+ onColumnFilter: onCellFilter
102103 )
103104 }
104105 }
@@ -200,6 +201,28 @@ struct LogHeaderView: View {
200201 @Binding var columnWidths: [String: CGFloat]
201202 let records: [LogRecord]
202203 var onMoveColumn: ((String, String) -> Void)?
204+ var onColumnFilter: ((_ column: String, _ value: JSONValue?, _ exclude: Bool) -> Void)?
205+
206+ private static let maxFilterValues = 20
207+
208+ private func uniqueValues(for column: String) -> [JSONValue] {
209+ var seen = Set<JSONValue>()
210+ var result: [JSONValue] = []
211+ for record in records {
212+ let value = record[column] ?? .null
213+ if seen.insert(value).inserted {
214+ result.append(value)
215+ }
216+ }
217+ return result.sorted()
218+ }
219+
220+ private func filterDisplayLabel(for value: JSONValue) -> String {
221+ let str = value.displayString
222+ if str.isEmpty { return "(empty)" }
223+ if str.count > 50 { return String(str.prefix(50)) + "..." }
224+ return str
225+ }
203226
204227 var body: some View {
205228 HStack(spacing: 0) {
@@ -262,6 +285,40 @@ struct LogHeaderView: View {
262285 Button("Auto-fit All Columns") {
263286 columnWidths = computeColumnWidths(columns: columns, records: records)
264287 }
288+
289+ if let onFilter = onColumnFilter {
290+ let values = uniqueValues(for: column)
291+ if !values.isEmpty {
292+ Divider()
293+
294+ let displayValues = Array(values.prefix(Self.maxFilterValues))
295+ let remaining = values.count - displayValues.count
296+
297+ Menu("Filter by Value") {
298+ ForEach(displayValues, id: \.self) { value in
299+ Button(filterDisplayLabel(for: value)) {
300+ onFilter(column, value == .null ? nil : value, false)
301+ }
302+ }
303+ if remaining > 0 {
304+ Divider()
305+ Text("\(remaining) more values not shown")
306+ }
307+ }
308+
309+ Menu("Exclude Value") {
310+ ForEach(displayValues, id: \.self) { value in
311+ Button(filterDisplayLabel(for: value)) {
312+ onFilter(column, value == .null ? nil : value, true)
313+ }
314+ }
315+ if remaining > 0 {
316+ Divider()
317+ Text("\(remaining) more values not shown")
318+ }
319+ }
320+ }
321+ }
265322 }
266323 }
267324 }