git.iko.soy/parseable-macos
on master help sign in
Fix 4 bugs: custom time range validation, health status flash, severity tinting lag, completion provider divergence
commit172566c4a75e16b4545b22a0f68912639b20cd1f authoriko <mail@iko.soy> dateSun 21 Jun 2026, 15:02 (3 months ago) parent96828ac
4 files changed+56−16
ParseableViewer/Views/LiveTailView.swift+7−4 ParseableViewer/Views/SQLAutoComplete.swift+20−6 ParseableViewer/Views/ServerInfoView.swift+19−6 ParseableViewer/Views/TimeRangePicker.swift+10
ParseableViewer/Views/LiveTailView.swift+7−4
@@ -311,10 +311,15 @@ struct LiveTailView: View {
311311 // wake after its 150ms delay and overwrite the unsorted result with
312312 // stale sorted entries.
313313 liveTailSortTask?.cancel()
314+ // Severity is keyed by entry id and is independent of row order, so
315+ // publish it synchronously in both paths. Deferring it into the
316+ // debounced sort task (as the sorted path used to) left rows tinted from
317+ // the previous severity-column set for ~150ms — or indefinitely if rapid
318+ // changes kept cancelling the task before it fired.
319+ severityColumnSet = sevCols
320+ severityByID = severityMap
314321 guard let col = sortColumn else {
315322 cachedSorted = entries
316− severityColumnSet = sevCols
317− severityByID = severityMap
318323 return
319324 }
320325 let asc = sortAscending
@@ -334,8 +339,6 @@ struct LiveTailView: View {
334339 }.value
335340 guard !Task.isCancelled else { return }
336341 cachedSorted = sorted
337− severityColumnSet = sevCols
338− severityByID = severityMap
339342 }
340343 }
341344
ParseableViewer/Views/SQLAutoComplete.swift+20−6
@@ -108,14 +108,28 @@ enum SQLCompletionProvider {
108108 let context = determineContext(text: text, position: wordStart)
109109
110110 // A table completion supplies its own surrounding quotes. If the user has
111− // already typed an opening double-quote (common for stream names with
112− // spaces, e.g. `FROM "my`), absorb it into the replacement range so the
113− // inserted `"name"` replaces the stray quote instead of producing `""name"`.
111+ // already typed quotes directly around the word (common for stream names
112+ // with spaces, e.g. `FROM "my"`), absorb them into the replacement range so
113+ // the inserted `"name"` replaces the stray quotes instead of producing
114+ // `""name""`. Mirror `insertCompletion`, which absorbs leading and trailing
115+ // quotes independently for any `.table` item — so apply this wherever table
116+ // completions are offered (`.tableRef` and `.general`), not just `.tableRef`.
114117 var replaceStart = wordStart
115− if case .tableRef = context, wordStart > 0, nsText.character(at: wordStart - 1) == 0x22 {
116− replaceStart -= 1
118+ var replaceEnd = wordEnd
119+ let offersTable: Bool
120+ switch context {
121+ case .tableRef, .general: offersTable = true
122+ default: offersTable = false
123+ }
124+ if offersTable {
125+ if wordStart > 0, nsText.character(at: wordStart - 1) == 0x22 {
126+ replaceStart -= 1
127+ }
128+ if wordEnd < nsText.length, nsText.character(at: wordEnd) == 0x22 {
129+ replaceEnd += 1
130+ }
117131 }
118− let range = NSRange(location: replaceStart, length: wordEnd - replaceStart)
132+ let range = NSRange(location: replaceStart, length: replaceEnd - replaceStart)
119133 let uppercasePrefix = prefix.uppercased()
120134
121135 var items: [SQLCompletionItem] = []
ParseableViewer/Views/ServerInfoView.swift+19−6
@@ -41,12 +41,25 @@ struct ServerInfoView: View {
4141 // Health status
4242 GroupBox("Health") {
4343 HStack {
44− Circle()
45− .fill(isHealthy ? .green : .red)
46− .frame(width: 12, height: 12)
47− .accessibilityLabel(isHealthy ? "Healthy" : "Unhealthy")
48− Text(isHealthy ? "Healthy" : "Unhealthy")
49− .fontWeight(.medium)
44+ // While the check is in flight, show a neutral state
45+ // rather than the default `isHealthy == false`, which
46+ // otherwise flashes a red "Unhealthy" dot on every
47+ // connection switch before the result resolves.
48+ if isLoading {
49+ Circle()
50+ .fill(.gray)
51+ .frame(width: 12, height: 12)
52+ .accessibilityLabel("Checking health")
53+ Text("Checking…")
54+ .fontWeight(.medium)
55+ } else {
56+ Circle()
57+ .fill(isHealthy ? .green : .red)
58+ .frame(width: 12, height: 12)
59+ .accessibilityLabel(isHealthy ? "Healthy" : "Unhealthy")
60+ Text(isHealthy ? "Healthy" : "Unhealthy")
61+ .fontWeight(.medium)
62+ }
5063 Spacer()
5164 }
5265 .padding(8)
ParseableViewer/Views/TimeRangePicker.swift+10
@@ -31,6 +31,16 @@ struct TimeRangePicker: View {
3131 .font(.caption)
3232 }
3333 }
34+ .onChange(of: showCustomPicker) { _, isShown in
35+ // Dismissing the popover by clicking outside bypasses the "Done"
36+ // button's `customEnd > customStart` validation, which could leave an
37+ // inverted or zero-width range selected (the menu already set
38+ // `option == .custom`). Sanitize on close so a backwards range can
39+ // never reach a query.
40+ if !isShown && customEnd <= customStart {
41+ customEnd = customStart.addingTimeInterval(3600)
42+ }
43+ }
3444 .popover(isPresented: $showCustomPicker) {
3545 VStack(spacing: 12) {
3646 Text("Custom Time Range")