git.iko.soy/parseable-macos
on master help sign in
Fix 5 bugs: wedged stream detail spinner, cross-server stale data, double rendering, contradictory tail filters, stale poll time
5433a10iko
ParseableViewer/ViewModels/LiveTailViewModel.swift+9−2
@@ -150,8 +150,12 @@ final class LiveTailViewModel {
150 }
150 }
151
151
152 func addColumnFilter(column: String, value: JSONValue?, exclude: Bool) {
152 func addColumnFilter(column: String, value: JSONValue?, exclude: Bool) {
153− // Remove any existing filter on the same column with the same value
153+ // Remove any existing filter on the same column+value regardless of its
154+ // exclude flag. Include (col = X) and exclude (col ≠ X) on the same value
155+ // are mutually exclusive; without dropping the `exclude` match too, toggling
156+ // include↔exclude stacks both and the AND'd filters match zero rows, so the
157+ // tail silently empties.
154− columnFilters.removeAll { $0.column == column && $0.value == value && $0.exclude == exclude }
158+ columnFilters.removeAll { $0.column == column && $0.value == value }
155 columnFilters.append(ColumnFilter(column: column, value: value, exclude: exclude))
159 columnFilters.append(ColumnFilter(column: column, value: value, exclude: exclude))
156 rebuildFilteredEntries()
160 rebuildFilteredEntries()
157 }
161 }
@@ -245,6 +249,9 @@ final class LiveTailViewModel {
245 // leaving accumulated errors in place could trip the auto-stop threshold
249 // leaving accumulated errors in place could trip the auto-stop threshold
246 // sooner than the intended N consecutive failures. Matches start()'s reset.
250 // sooner than the intended N consecutive failures. Matches start()'s reset.
247 consecutiveErrors = 0
251 consecutiveErrors = 0
252+ // Match start(): a freshly emptied tail has no last-poll timestamp, so any
253+ // "last updated at…" indicator shouldn't keep showing the pre-clear time.
254+ lastPollTime = nil
248 columns = []
255 columns = []
249 columnOrder = []
256 columnOrder = []
250 hiddenColumns = []
257 hiddenColumns = []
ParseableViewer/Views/LogDetailView.swift+5−2
@@ -157,8 +157,11 @@ struct JSONValueView: View {
157 Text(String(v))
157 Text(String(v))
158 .font(.system(.caption, design: .monospaced))
158 .font(.system(.caption, design: .monospaced))
159 .foregroundStyle(Color(nsColor: .systemPurple))
159 .foregroundStyle(Color(nsColor: .systemPurple))
160− case .double(let v):
160+ case .double:
161− Text(String(v))
161+ // Use displayString so whole-number doubles render as "3" (not "3.0"),
162+ // scientific notation is avoided, and inf/nan map to "null" — matching
163+ // the table cells, CSV/JSON export, and the rest of the app.
164+ Text(value.displayString)
162 .font(.system(.caption, design: .monospaced))
165 .font(.system(.caption, design: .monospaced))
163 .foregroundStyle(Color(nsColor: .systemPurple))
166 .foregroundStyle(Color(nsColor: .systemPurple))
164 case .string(let v):
167 case .string(let v):
ParseableViewer/Views/StreamDetailView.swift+17−3
@@ -157,7 +157,11 @@ struct StreamDetailView: View {
157 }
157 }
158 .padding()
158 .padding()
159 }
159 }
160+ // Key on the connection too: switching to a different server that
161+ // exposes a same-named stream leaves `stream` unchanged, so without
162+ // the connection id the task wouldn't refire and the old server's
163+ // data would linger.
160− .task(id: stream) {
164+ .task(id: "\(appState.activeConnection?.id.uuidString ?? "")|\(stream)") {
161 await loadData(stream: stream)
165 await loadData(stream: stream)
162 }
166 }
163 } else {
167 } else {
@@ -172,6 +176,11 @@ struct StreamDetailView: View {
172
176
173 private func loadData(stream: String) async {
177 private func loadData(stream: String) async {
174 guard let client = appState.client else { return }
178 guard let client = appState.client else { return }
179+ // Capture the connection this fetch belongs to. Two servers can expose a
180+ // same-named stream, so guarding on the stream name alone would let a
181+ // slow fetch write the previous server's data under the new server's
182+ // stream after a server switch.
183+ let connID = appState.activeConnection?.id
175 isLoading = true
184 isLoading = true
176 errorMessage = nil
185 errorMessage = nil
177 // Clear previous stream's data so it isn't shown under the new header
186 // Clear previous stream's data so it isn't shown under the new header
@@ -205,8 +214,13 @@ struct StreamDetailView: View {
205 // The manual Refresh/Retry buttons spawn detached Tasks that aren't tied to
214 // The manual Refresh/Retry buttons spawn detached Tasks that aren't tied to
206 // `.task(id: stream)` cancellation, so a slow refresh for the old stream can
215 // `.task(id: stream)` cancellation, so a slow refresh for the old stream can
207 // resolve after the user switched. Drop stale results instead of writing
216 // resolve after the user switched. Drop stale results instead of writing
208− // them under the new stream's header.
217+ // them under the new stream's header. Also check the connection so a
218+ // same-named stream on a different server doesn't accept the old server's
219+ // data. Reset `isLoading` on the stale path or the spinner wedges forever.
209− guard stream == appState.selectedStream else { return }
220+ guard stream == appState.selectedStream, connID == appState.activeConnection?.id else {
221+ isLoading = false
222+ return
223+ }
210
224
211 schema = newSchema
225 schema = newSchema
212 stats = newStats
226 stats = newStats