Skip to content

Hide routine timeline activity behind a visible toggle, never problems - #1924

Merged
hisco merged 5 commits into
mainfrom
radar-timeline-hidden-rows
Oct 7, 2026
Merged

hisco merged 5 commits into
mainfrom
radar-timeline-hidden-rows

Conversation

@hisco

@hisco hisco commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

The Timeline list hides "managed" rows (pods, replica sets, Event-kind rows, anything with an owner) to stay readable, and nothing on screen says so. You can check this on main today:

  • A namespace whose only trouble is a crash-looping bare pod: the list with Problems on shows no rows.
  • A crash-looping pod under a Deployment: the list shows the Deployment going unhealthy. The pod's BackOff and OOMKilled rows are hidden, and the Problems chip leaves them out of its count.
  • Kinds → Job lists Jobs a CronJob created, which the unfiltered list never shows, with no hint why a filter shows more.

Before and after

A single crash-looping pod with no Deployment above it, Problems filter on. Main shows "No activity found"; with this change the crash is listed and the hidden routine count is shown.

pr1924-solo-before-after

Ten crash-looping pods, Kinds → Pod. Main lists every pod event, including routine steps of healthy pods; with this change each crashing pod is one card with the rest of its history folded under it.

pr1924-stress-before-after

What changes

The list now splits its rows three ways:

Rows Default
Owner-level rows (Deployments, CronJobs, standalone Jobs, ...) shown
Problems on child resources, using the Problems chip's own definition (isTimelineProblem) always shown
Routine child activity: pods created/scheduled/started, replica sets scaling, a CronJob's successful Jobs hidden
  • "Show routine activity" checkbox in a slim row under the toolbar. It has an (i) tooltip that explains what routine means and a count ("· 321 hidden" / "· 321 shown"). The choice persists as routine=1 in the URL.
  • The checkbox alone decides. Picking a kind no longer turns the hiding off; this replaces the rule from fix(timeline): list managed rows when a kind is selected #1891. Kinds → Pod shows the pod problems plus the count of hidden routine pod rows.
  • Child problems fold per resource. While routine activity is hidden, a child resource's problem rows in one time group fold under its newest card: "3 more on this Pod: BackOff ×45, 2 status changes". The line expands to the actual rows.
  • Empty state. When every matching row is routine, the list says so and offers the checkbox's action.

The Applications-scoped list and the swimlane are unchanged.

Where to look hardest

  • splitRoutineActivity (web/src/components/timeline/TimelineList.tsx). One uncapped query over the already-loaded ring, so no extra fetch. It splits the rows into three buckets, each capped at the list limit. Three invariants rest on it:
    • owner rows can't crowd child problems out of the cap;
    • checking the box only ever adds rows;
    • the count equals the rows it adds.
  • The count is computed in k8s-ui, through the same activity, kind, deleted and search filters the list applies. Those filters run after the host's cap, so the host can't count alone.
  • Folded groups are keyed by resource and time group, not by their newest row. That keeps an opened fold open as live events arrive. A fold also opens when it contains selectedEventId, and every folded row keeps data-event-id, so selection from the swimlane still lands.
  • The checkbox gets its own row because the filter row has no room. A toolbar chip overflowed the row by 142px at 1440px wide. Main already overflows by about 25px at 1280.

Evidence

Measured on a kind cluster.

  • Folding (10 crash-looping pods, about 7 minutes of history): without folding, the default list held 515 rows. With folding, 51 cards, about two per pod.
  • Count accuracy: the rows the checkbox added matched its count exactly in six scenarios:
    • the default list
    • Problems on
    • Kinds → Pod
    • a search
    • a namespace with one crash-looping bare pod
    • the crash-loop namespace
  • Cost on a synthetic 50,000-row ring: 10.9 ms per update on main (capped query) vs 16.7 ms with the uncapped query and the split.
  • List size: at most 4,000 rows (2,000 owner rows plus 2,000 child problems). With routine shown, 6,000.

Not in scope

  • Deployment health flips ("ready 1→0" then "0→1" on every pod restart) are not folded. That's existing behavior, and on a busy namespace these rows are now the loudest thing in the list. Folding them is a separate change.
  • The histogram strip still counts all activity regardless of filters.
  • The truncation note still doesn't show when filters empty the list after the cap. Also existing behavior.

k8s-ui API

  • New optional TimelineList props: showRoutine, onShowRoutineChange, routineEvents, foldPerResource.
  • New export: isTimelineProblem.
  • Nothing removed or renamed.

Tests

  • New unit tests for the split: owner saturation, the checkbox only adding rows, per-kind behavior.
  • New tests for the toggle, the count under filters, the empty state, folding, and selection opening a fold.
  • npm test in web/: 148 files, 1708 tests. k8s-ui: 4036 tests. make tsc is clean.

Note

Medium Risk
Changes default timeline visibility and list fetch/cap logic; problems must stay visible via shared isTimelineProblem, with broad unit test coverage but meaningful UX and performance impact on large rings.

Overview
The timeline list hides routine child activity by default (pod starts, replica set churn, successful CronJob jobs) while always showing owner-level rows and child problems aligned with the Problems filter via new isTimelineProblem.

The web host adds splitRoutineActivity: one uncapped fetch over the loaded ring, separate 2k caps per bucket so owner bursts cannot displace child problems, and opting in only adds rows. routine=1 persists the Show routine activity toggle in the URL.

k8s-ui TimelineList gains optional routine props (checkbox row with filter-aware hidden/shown counts), an empty state when everything matching is routine, and foldPerResource to collapse multiple problem events per resource into one card with an expandable “N more on this Pod” line (when routine is hidden).

Replaces the old rule where selecting a kind re-included all managed rows. Bumps @skyhook-io/k8s-ui peer to >=1.16.3. Application-scoped lists and swimlanes are unchanged.

Reviewed by Cursor Bugbot for commit d3b9282. Bugbot is set up for automated code reviews on this repo. Configure here.

The Timeline list hid pods, replica sets and owned rows without saying so,
including their warnings and failures. Split the list into owner rows,
child problems (always shown) and routine child activity, shown on request
through a "Show routine activity" checkbox with an exact count. Child
problems fold per resource under their newest card so a crash-looping pod
stays one entry. Picking a kind no longer overrides the toggle.
@hisco
hisco requested a review from nadaverell as a code owner September 29, 2026 12:54
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Show child-resource problems while hiding routine timeline activity

🐞 Bug fix âœĻ Enhancement 🧊 Tests 🕐 40+ Minutes

Grey Divider

AI Description

â€Ē Keep child-resource problems visible while hiding routine activity behind an explicit toggle.
â€Ē Show a filter-aware routine count, fold repeated child problems, and persist the choice in the
 URL.
â€Ē Preserve application-scoped behavior and test row caps, filtering, folding, and URL state.
Diagram

graph TD
  R["Loaded event ring"] --> M{"Managed row?"} -- "Yes" --> P{"Problem?"} -- "Yes" --> C["Child problems"] --> L["Timeline list"]
  M -- "No" --> O["Owner rows"] --> L
  P -- "No" --> H["Routine rows"] -- "Count or show" --> L
  S["URL toggle"] --> L
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Split rows at the source
  • ➕ Could avoid scanning and sorting the full loaded window in the list host.
  • ➖ Requires source-specific query and cap changes; separate results complicate count accuracy and consistent behavior.
2. Fetch routine activity separately
  • ➕ Keeps the default visible-row query small.
  • ➖ Adds fetching and synchronization work, while making the hidden count harder to keep exact.

Recommendation: Use the PR's client-side split over the already-loaded ring. Separate bucket caps protect child problems and make opting in additive without another fetch; the measured scan cost is a reasonable trade-off. Review the ring's coverage limits when interpreting counts.

Files changed (9) +407 / -65

Enhancement (3) +195 / -25
TimelineList.tsxAdd routine toggle, filtered count, and child-problem folds +185/-23

Add routine toggle, filtered count, and child-problem folds

â€Ē Adds optional host-controlled routine activity props, a dedicated checkbox row with an explanation and count, and an actionable routine-only empty state. Folds eligible rows by resource within each time group while retaining selection targets and visible-window timestamps.

packages/k8s-ui/src/components/timeline/TimelineList.tsx

index.tsExport the shared timeline problem predicate +1/-0

Export the shared timeline problem predicate

â€Ē Exposes isTimelineProblem to hosts through the timeline package entry point.

packages/k8s-ui/src/components/timeline/index.ts

TimelineView.tsxPersist routine visibility in timeline URL state +9/-2

Persist routine visibility in timeline URL state

â€Ē Initializes and synchronizes the routine toggle with the optional routine=1 parameter, then passes the state and setter to the list.

web/src/components/timeline/TimelineView.tsx

Bug fix (2) +61 / -10
timeline-filters.tsDefine problems using the existing Problems filter +7/-0

Define problems using the existing Problems filter

â€Ē Adds isTimelineProblem using the same warning and unhealthy activity keys as the Problems chip, so hosts can preserve those rows when hiding routine activity.

packages/k8s-ui/src/components/timeline/timeline-filters.ts

TimelineList.tsxSplit unscoped timeline rows before applying per-bucket caps +54/-10

Split unscoped timeline rows before applying per-bucket caps

â€Ē Queries the loaded window without a list cap, then independently caps owner rows, child problems, and routine rows. Passes the selected routine rows and folding predicate to k8s-ui while leaving the application-scoped list unsplit.

web/src/components/timeline/TimelineList.tsx

Refactor (1) +1 / -1
timelineSource.tsExpose managed-row classification to the timeline host +1/-1

Expose managed-row classification to the timeline host

â€Ē Exports the existing managed-event predicate for reuse by the list split without changing its classification rules.

web/src/api/timelineSource.ts

Tests (3) +150 / -29
TimelineToolbarState.test.tsxTest routine activity controls and folded-row rendering +97/-0

Test routine activity controls and folded-row rendering

â€Ē Covers toggle visibility, counts under kind filters, shown and empty states, fold summaries, and automatic expansion for a selected folded event.

packages/k8s-ui/src/components/timeline/TimelineToolbarState.test.tsx

TimelineList.test.tsVerify three-way row splitting and independent caps +47/-29

Verify three-way row splitting and independent caps

â€Ē Replaces kind-based managed-row tests with cases for owner rows, child problems, routine rows, kind selections, newest-first ordering, cap isolation, and additive opt-in.

web/src/components/timeline/TimelineList.test.ts

TimelineView.urlparams.test.tsTest routine URL serialization and history behavior +6/-0

Test routine URL serialization and history behavior

â€Ē Adds routine visibility to URL parse/write fixtures and round-trip coverage, and verifies that changing it is not treated as a high-frequency history replacement.

web/src/components/timeline/TimelineView.urlparams.test.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/timeline/TimelineList.tsx
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) ðŸŽĻ UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Recreated resources share a problem card 🐞 Bug ≡ Correctness
Description
aggregateEvents keys folded problems by kind, namespace, and name but omits the resource UID. When
a resource is deleted and recreated under the same name within one time group, problems from both
incarnations appear under the newest resource's card.
Code

packages/k8s-ui/src/components/timeline/TimelineList.tsx[233]

+        const key = `fold:${item.kind}:${item.namespace}:${item.name}`
Evidence
Both informer rows and Kubernetes Event rows carry the affected resource's UID; recreation gives the
replacement a different UID. The new key discards that discriminator before rendering all matching
rows as one fold.

packages/k8s-ui/src/components/timeline/TimelineList.tsx[231-236]
packages/k8s-ui/src/components/timeline/TimelineList.tsx[260-264]
pkg/timeline/converter.go[34-96]
internal/k8s/cache.go[973-1017]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fold keys merge problems from distinct resources that reuse a kind, namespace, and name.
## Fix Focus Areas
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[231-236]
## Recommended Fix
Use the subject resource UID in the fold key when present, retaining a name-based fallback for rows without a UID. Account for API-group collisions without splitting equivalent served versions of one resource.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Expanding problems leaves the lens stale 🐞 Bug ≡ Correctness
Description
FoldedProblems mounts and unmounts timestamped rows when its button changes open, but that
transition does not call reportVisibleWindow. If expansion changes which rows intersect the list
viewport without a scroll event, the host's visible-window lens retains its previous range until
another reporting trigger occurs.
Code

packages/k8s-ui/src/components/timeline/TimelineList.tsx[R873-875]

+      <Collapse open={open}>
+        {open && <div className="mt-2 space-y-2 border-l-2 border-theme-border pl-3">{others.map((e) => (
+          <div key={e.id} data-event-id={e.id} data-ts-from={new Date(e.timestamp).getTime()} data-ts-to={new Date(e.timestamp).getTime()}>
Evidence
The window is measured from timestamped elements intersecting the viewport. Existing reports run on
scroll or when grouped activity changes, whereas the new fold changes its own local state and
conditionally mounts rows without changing that dependency.

packages/k8s-ui/src/components/timeline/TimelineList.tsx[363-399]
packages/k8s-ui/src/components/timeline/TimelineList.tsx[858-879]
web/src/components/timeline/TimelineView.tsx[1037-1052]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Opening or closing a problem fold changes the visible rows without necessarily updating the host's visible-window lens.
## Fix Focus Areas
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[858-880]
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[363-389]
## Recommended Fix
Schedule visible-window reporting after a fold opens or closes and its layout has updated, including folds opened by external selection.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Folded rows cannot be collapsed while selected ✓ Resolved
Description
FoldedProblems computes open = userOpen || selectionInside, but the button only toggles
userOpen via setOpen(!open). When the fold is open because it holds selectedEventId, clicking
calls setOpen(false), which changes nothing, so the fold stays open while the chevron and
aria-expanded say it can close.
Code

packages/k8s-ui/src/components/timeline/TimelineList.tsx[R859-866]

+  const [userOpen, setOpen] = useState(false)
+  // A selection made elsewhere (the swimlane) must be able to land on a folded row.
+  const open = userOpen || (!!selectedEventId && others.some((e) => e.id === selectedEventId))
+  return (
+    <div className="pl-3">
+      <button
+        type="button"
+        onClick={() => setOpen(!open)}
Evidence
userOpen starts false. When the selection is inside the fold, open is true, so the click calls
setOpen(false): no state change and no re-render, and the fold stays open.

packages/k8s-ui/src/components/timeline/TimelineList.tsx[858-874]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In FoldedProblems, `open` is `userOpen || selectionInside`, and clicking sets `userOpen = !open`. While the selected row is inside the fold, the button cannot collapse it.
## Fix Focus Areas
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[858-872]
## Recommended Fix
Change the state to `useState<boolean | null>(null)` and compute `const open = userOpen ?? selectionInside`. Keep the click handler as `setOpen(!open)`. Optionally reset the state to `null` when `selectedEventId` changes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Truncation note understates rows shown ✓ Resolved
Description
splitRoutineActivity caps owner rows, child problems, and optional routine rows separately, but
truncatedAt={fetchLimit} still makes the list say it shows the “newest 2,000 events.” When
multiple buckets hit their 2,000-row caps, the list can contain 4,000–6,000 rows, and buckets can
reach different points in time, leaving older groups with child problems but no owner rows from the
same period.
Code

web/src/components/timeline/TimelineList.tsx[R48-55]

+  const base = [...owners.slice(0, limit), ...childProblems.slice(0, limit)].sort(byNewest)
+  const shownRoutine = routine.slice(0, limit)
+  return {
+    withoutRoutine: base,
+    withRoutine: [...base, ...shownRoutine].sort(byNewest),
+    routine: shownRoutine,
+    truncated: owners.length > limit || childProblems.length > limit,
+    routineTruncated: routine.length > limit,
Evidence
The source query is uncapped, and splitRoutineActivity slices each category independently before
merging the rows. The shared list formats truncatedAt, still set to fetchLimit, as a single
total cap, so its default note does not describe either the possible row count or the differing time
ranges.

web/src/components/timeline/TimelineList.tsx[40-57]
packages/k8s-ui/src/components/timeline/TimelineList.tsx[615-618]
web/src/components/timeline/TimelineList.tsx[109-127]
web/src/components/timeline/TimelineList.tsx[40-56]
web/src/components/timeline/TimelineList.tsx[203-209]
packages/k8s-ui/src/components/timeline/TimelineList.tsx[614-619]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The list applies separate caps to owner rows, child problems, and optional routine rows, but its truncation note describes one global cap of 2,000 newest events. The resulting 4,000–6,000 rows can also cover different time ranges across categories.
## Fix Focus Areas
- web/src/components/timeline/TimelineList.tsx[40-57]
- web/src/components/timeline/TimelineList.tsx[137-139]
- web/src/components/timeline/TimelineList.tsx[205-209]
## Recommended Fix
Pass a `truncationMessage` that describes the per-category limits and displayed rows, including the optional routine category. Alternatively, trim every bucket to the oldest timestamp that all truncated buckets still cover so the displayed time range is consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Keyboard users cannot open routine info ✓ Resolved
Description
The (i) tooltip trigger is a bare Info SVG with no tabIndex and no button wrapper, so it never
takes focus. The only explanation of what "routine" hides is therefore mouse-only.
Code

packages/k8s-ui/src/components/timeline/TimelineList.tsx[R831-833]

+      <Tooltip content={ROUTINE_ACTIVITY_INFO} delay={150} position="bottom" wrapperClassName="shrink-0">
+        <Info className="h-3.5 w-3.5 cursor-default text-theme-text-tertiary/70 hover:text-theme-text-secondary" aria-label="About routine activity" />
+      </Tooltip>
Evidence
The icon is rendered directly as the Tooltip child, with no focusable element around it.

packages/k8s-ui/src/components/timeline/TimelineList.tsx[831-833]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Info icon is not focusable, so keyboard users cannot open the tooltip.
## Fix Focus Areas
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[831-833]
## Recommended Fix
Wrap the icon in `<button type="button" aria-label="About routine activity">` inside the Tooltip, and make sure the Tooltip also opens on focus.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
ðŸ’Ą Did you know, you can route each severity your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo


for (const item of items) {
if (foldPerResource?.(item)) {
const key = `fold:${item.kind}:${item.namespace}:${item.name}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Recreated resources share a problem card 🐞 Bug ≡ Correctness

aggregateEvents keys folded problems by kind, namespace, and name but omits the resource UID. When
a resource is deleted and recreated under the same name within one time group, problems from both
incarnations appear under the newest resource's card.
Agent Prompt
## Issue description
Fold keys merge problems from distinct resources that reuse a kind, namespace, and name.
## Fix Focus Areas
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[231-236]
## Recommended Fix
Use the subject resource UID in the fold key when present, retaining a name-based fallback for rows without a UID. Account for API-group collisions without splitting equivalent served versions of one resource.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +873 to +875
<Collapse open={open}>
{open && <div className="mt-2 space-y-2 border-l-2 border-theme-border pl-3">{others.map((e) => (
<div key={e.id} data-event-id={e.id} data-ts-from={new Date(e.timestamp).getTime()} data-ts-to={new Date(e.timestamp).getTime()}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Expanding problems leaves the lens stale 🐞 Bug ≡ Correctness

FoldedProblems mounts and unmounts timestamped rows when its button changes open, but that
transition does not call reportVisibleWindow. If expansion changes which rows intersect the list
viewport without a scroll event, the host's visible-window lens retains its previous range until
another reporting trigger occurs.
Agent Prompt
## Issue description
Opening or closing a problem fold changes the visible rows without necessarily updating the host's visible-window lens.
## Fix Focus Areas
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[858-880]
- packages/k8s-ui/src/components/timeline/TimelineList.tsx[363-389]
## Recommended Fix
Schedule visible-window reporting after a fold opens or closes and its layout has updated, including folds opened by external selection.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread packages/k8s-ui/src/components/timeline/TimelineList.tsx Outdated
Comment thread web/src/components/timeline/TimelineList.tsx
Comment thread packages/k8s-ui/src/components/timeline/TimelineList.tsx

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/k8s-ui/src/components/timeline/TimelineList.tsx Outdated
hisco and others added 2 commits September 29, 2026 16:18
â€Ķthe k8s-ui floor

The fold panel unmounted its rows the moment it closed, so it snapped
shut instead of animating; Collapse with unmountOnExit keeps them through
the close transition.

FoldedProblems opened itself for a selection inside it, but no caller
passes both foldPerResource and selectedEventId, so that path never ran.
A plain open state replaces it.

radar-app now imports isTimelineProblem, which no published k8s-ui has
yet, so the peer floor moves to the next k8s-ui release.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d3d1a3b. Configure here.

Comment thread packages/k8s-ui/src/components/timeline/TimelineList.tsx
â€Ķ-rows

# Conflicts:
#	package-lock.json
#	web/package.json
@hisco
hisco merged commit 1f05aee into main Oct 7, 2026
9 of 10 checks passed
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