Skip to content

🐛 Stop the refresh button from jumping when a query is in progress - #3117

Closed
johnsimons wants to merge 19 commits into
ramon/audit-incomplete-resultsfrom
john/stop_jumpiness
Closed

johnsimons wants to merge 19 commits into
ramon/audit-incomplete-resultsfrom
john/stop_jumpiness

Conversation

@johnsimons

Copy link
Copy Markdown
Member

Reviewer Checklist

  • Components are broken down into sensible and maintainable sub-components.
  • Styles are scoped to the component using it. If multiple components need to share CSS, then a .css file is created containing the shared CSS and imported into component scoped style sections.
  • Naming is consistent with existing code, and adequately describes the component or function being introduced
  • Only functions utilizing Vue state or lifecycle hooks are named as composables (i.e. starting with 'use');
  • No module-level state is being introduced. If so, request the PR author to move the state to the corresponding Pinia store.

ramonsmits and others added 19 commits September 7, 2026 17:43
The very-long query on first view access was not the message list (which
is bounded by the default time range) but the hasSuccessfulMessages
capability probe: an UNBOUNDED sorted query over the whole audit index,
re-fired every 5 seconds by the platform-capabilities poll. It now
probes the last 7 days first, falls back to unbounded only when that
window is empty, and stops querying entirely once successful messages
have been seen — they don't un-exist.
The 'no successful audit messages' onboarding banner and dashboard card
assumed 'no messages' by default (the flag started false) and un-rendered
once the probe proved otherwise — on large stores that meant minutes of a
wrong onboarding prompt, and on every fresh session a flicker.

hasSuccessfulMessages is now tri-state: null (not yet determined),
false (a completed probe found nothing), true. While unknown the
auditing capability reports a new Checking status: the All Messages
banner renders nothing and the dashboard card shows its loading overlay.
A failed probe proves nothing about the data, so it leaves the state
unknown and the next tick retries instead of claiming 'no messages'.
Show (page size) and Sort are result-presentation controls, not query
filters — they now sit right of 'Showing X of Y result(s)' instead of
competing with the query inputs for space in the filters panel, which
also removes their awkward wrapping. The filters panel keeps the query
inputs only (search, endpoint, time range), left-aligned; the rule that
pushed the last filter to the far right is gone.
Large audit stores easily reach nine-digit totals; 'Showing 100 of
158736340 result(s)' is unreadable. Both numbers now go through
Intl.NumberFormat with the user's locale (158,736,340 / 158.736.340).
ResultsCount is shared, so the heartbeats views and the throughput
queue list pick this up as well.
The refresh cluster occupied its own row above the filters; it now sits
right-aligned inside the query bar so the whole query surface is one
row. While a query is in flight the button flips from Refresh to an
enabled Cancel that ticks the elapsed time (Cancel · 3.2s) and aborts
the running query through the store — the abort propagates through
ServiceControl and terminates the database-side query. Once results
land, the results line reports what they cost: 'Showing 100 of
158,736,340 result(s) · took 2.7 s'.

ActionButton hard-coupled loading to disabled, which would have left
the button dead exactly when a user most wants an escape hatch; it
gains disableOnLoading (default true, existing uses unchanged) and the
refresh/cancel button opts out. The spec stubs ActionButton with the
real disabled semantics so that coupling can't silently return.
While auto-refresh is armed and no query runs, the refresh arrow is
replaced by a small ring that depletes toward the next refresh, with
the exact remaining seconds as its tooltip; during a query the button
shows the usual spinner and Cancel. The ring mirrors FontAwesome's
icon geometry — ActionButton gains a named icon slot so custom icons
are flex children with the same centering as FAIcon, and the ring's
circle fills its box like a glyph — measured pixel-identical to the
arrow state in both axes. Reduced motion disables the animation.
The countdown ring inside the refresh button was an SVG carrying a
label that changed every second, which became part of the button's
accessible name. The ring is now purely decorative (aria-hidden) and the
countdown is a visually hidden "Next auto refresh in N seconds" timer
that describes the button: read together with it, not announced on
every tick. The button keeps its stable name "Refresh".
Leaving the view clears the rows; the "took X s" that described them
must not survive to the next visit.
Any query that used search text or an endpoint is remembered in this
browser (most recent first, capped, no duplicates) behind a History
control, with the time-range expressions it ran with: the range is part
of a search's identity, entries show it, and rerunning restores it so a
history entry is a true "run this exact query again".
Review feedback: a separate "History" dropdown sat next to the search
field yet listed whole queries, which felt disconnected. The history now
wraps the search field and floats below it when the field gets focus,
the way a browser address bar does. Typing narrows the list to entries
whose search text or endpoint match; Escape dismisses it and a click in
the field brings it back. Each entry still carries the full query
(text, endpoint, time range) and reruns all of it. An empty history
shows nothing.
Time Sent adapts to age — time only for today, 'yesterday 14:32' and
weekday names for the past week, the regular browser format beyond —
with a live coarse relative label (moments/minutes/hours/days/weeks/
months/years ago) and the full timestamp in both zones as tooltip. A
'Times: local|UTC' toggle on the results line flips every timestamp to
UTC wall-clock time (day boundaries included), persisted per browser.
The results line also shows when the current results were fetched:
'ran 3 minutes ago', with the exact timestamp as tooltip.
The audit row's grid gave every column an equal share, so short metric
cells sat on unused whitespace while a long Time Sent value wrapped
mid-label — and because every row was its own grid, a row with
'yesterday 8:22:30 PM' laid out differently from its neighbours.

The columns are now defined once on the list and shared by every row
via subgrid: the message type owns the top line; the id, the three
metrics and the sent time share the bottom line, bottom aligned. Data
columns never shrink below the widest value in the list (nothing wraps)
but share surplus width equally, so they stretch across the row on wide
screens. The age sits in its own content-sized column, right aligned so
every 'ago' ends at the same edge, and is hidden below 62rem — the
absolute timestamp carries the information. AdaptiveTimestamp gained a
part prop so the absolute time and the age can occupy separate grid
cells while both keep the dual-zone tooltip.
Leaving the view clears the rows; "ran X ago" described those rows and
must not show for an empty list on the next visit.
Under auto-refresh new messages simply appeared at the top of the list
with nothing to tell them apart from the rows already there. No results
view in ServicePulse had an arrival animation to reuse (the saga
diagram's blink-border is the only one-shot animation and targets
diagram nodes), so this adds one for All Messages.

The store now records which ids of a result were absent from the
previous result of the same query. The list marks those rows and a
one-shot CSS animation slides them in and lets a blue tint fade over
three seconds; the element is new to the DOM (rows are keyed by id), so
it plays once on insertion. Nothing animates on the first result or
when the query itself changed: then every row is different and
highlighting all of them says nothing. A failed query resets the
baseline, so the recovery result does not light up either. Reduced
motion keeps the tint but drops the movement.
After the view is left and re-entered the first result is a fresh start,
not "everything is new compared to the empty list".
ServiceControl (Particular/ServiceControl#5848) now answers a partial
composite with the X-Particular-Incomplete-Results header naming every
instance whose data is missing (instanceId:timeout|unavailable|error),
and a fully timed-out query with a 504. All Messages now reads both: a
partial page keeps its data but shows a 'Partial results' warning naming
each missing instance and its reason, and a 504 gets a precise 'exceeded
the ServiceControl query time limit' message instead of the generic
failure text. The header parser is shared so the conversation and saga
views can adopt it next.
ServiceControl identifies an audit instance by its API URL, lower-cased
and base64 encoded with the URL-safe alphabet ('-', '_' and '.' for
'+', '/' and '='), and that is what the X-Particular-Incomplete-Results
header carries. The partial-results warning printed the id as-is, so a
reader saw "No data from aHR0cDovL2xvY2FsaG9zdDo0NDQ0NC9hcGkv (timed out)".

The id is now decoded and shown as host and port (default port
omitted), with the full API URL as tooltip: "No data from
localhost:44444 (timed out)". An id that is not a base64 http(s) URL is
shown unchanged, so nothing depends on the encoding staying that way.
Leaving the view clears the rows; the partial-results warning and the
time-limit flag described those rows.
The elapsed-time label ("Cancel · 1.2s") updated every 100 ms, causing the button to resize continuously while a query ran. Removing the label and fixing the button width keeps it stable.
@ramonsmits
ramonsmits force-pushed the ramon/audit-incomplete-results branch 4 times, most recently from 14d4134 to 27899a2 Compare September 9, 2026 10:19
@ramonsmits

Copy link
Copy Markdown
Member

@ramonsmits ramonsmits closed this Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants