git.iko.soy/parseable-macos
on master help sign in
Add column header right-click filtering for query and live tail views (#16)
a99b1f3iko
ParseableViewer/Views/LiveTailView.swift+3
@@ -301,6 +301,9 @@ struct LiveTailView: View {
301 records: viewModel.filteredEntries.map { $0.record },
301 records: viewModel.filteredEntries.map { $0.record },
302 onMoveColumn: { from, to in
302 onMoveColumn: { from, to in
303 viewModel.moveColumn(from, to: to)
303 viewModel.moveColumn(from, to: to)
304+ },
305+ onColumnFilter: { column, value, exclude in
306+ viewModel.addColumnFilter(column: column, value: value, exclude: exclude)
304 }
307 }
305 )
308 )
306 }
309 }
ParseableViewer/Views/LogTableView.swift+58−1
@@ -98,7 +98,8 @@ struct LogTableView: View {
98 sortAscending: $sortAscending,
98 sortAscending: $sortAscending,
99 columnWidths: $columnWidths,
99 columnWidths: $columnWidths,
100 records: records,
100 records: records,
101− onMoveColumn: onMoveColumn
101+ onMoveColumn: onMoveColumn,
102+ onColumnFilter: onCellFilter
102 )
103 )
103 }
104 }
104 }
105 }
@@ -200,6 +201,28 @@ struct LogHeaderView: View {
200 @Binding var columnWidths: [String: CGFloat]
201 @Binding var columnWidths: [String: CGFloat]
201 let records: [LogRecord]
202 let records: [LogRecord]
202 var onMoveColumn: ((String, String) -> Void)?
203 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+ }
203
226
204 var body: some View {
227 var body: some View {
205 HStack(spacing: 0) {
228 HStack(spacing: 0) {
@@ -262,6 +285,40 @@ struct LogHeaderView: View {
262 Button("Auto-fit All Columns") {
285 Button("Auto-fit All Columns") {
263 columnWidths = computeColumnWidths(columns: columns, records: records)
286 columnWidths = computeColumnWidths(columns: columns, records: records)
264 }
287 }
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+ }
265 }
322 }
266 }
323 }
267 }
324 }