feat: redesign Jobs List page with stats dashboard, rich table, and next-run fix#73
feat: redesign Jobs List page with stats dashboard, rich table, and next-run fix#73wicky-zipstack wants to merge 8 commits intomainfrom
Conversation
Stats cards: - Active Jobs (with paused count) - Success Rate (24H) — computed client-side - Failed Runs (24H) with attention indicator - Next Run (countdown + job name) Table columns redesigned: - Job: icon + name (link) + description - Environment: PROD/STG/DEV styled badges with env name - Schedule: CRON/INTERVAL tags with expression + description - Last Run: status tag + date + relative time + duration - Next Run: date + "in Xm" countdown badge, "Paused" when disabled - Status: toggle switch + label - Actions: Run now, History, Edit, Delete icons Filters redesigned: - Inside bordered Card (matches Run History) - Search, Status, Last run, Environment, Schedule dropdowns - Job count + Clear filters link + Refresh Layout: matches Run History page width pattern
- CheckCircleFilled green bg → "Healthy — last run succeeded" - CloseCircleFilled red bg → "Last run failed — needs attention" - PauseCircleOutlined grey bg → "Paused — will not run" - ClockCircleOutlined blue bg + pulse → "Scheduled — has not run yet" - SyncOutlined spinning blue bg + pulse → "Running" - Circular icon with 12% opacity background matching status color - Tooltip on hover with descriptive status message
…n_at remaining_estimate(last_run_at) returns a negative duration when last_run_at is old, producing a past timestamp and hiding the countdown tag in the Jobs List UI.
|
| Filename | Overview |
|---|---|
| backend/backend/core/scheduler/views.py | Fixes _compute_next_run_time() to always use timezone.now() as the reference, preventing stale past timestamps; also inverts priority to prefer fresh computation over cached next_run_time |
| frontend/src/ide/scheduler/JobDeploy.css | Adds .jl-job-icon status variant classes (success/failed/running/paused) and @Keyframes jl-pulse animation for the running state indicator |
| frontend/src/ide/scheduler/JobList.jsx | Adds stats dashboard (active jobs, 24h success rate, 24h failed runs, next-run countdown), moves Create Job to page header, resolves lastRun filter and runSearch race condition from previous review; stats are still computed from backup which is page-scoped |
| frontend/src/ide/scheduler/JobListFilters.jsx | Redesigned to a Card-wrapped filter bar with Status, Last Run, Environment, and Schedule dropdowns; project filter UI removed while the backend filter logic in JobList.jsx still references filters.proj — project filtering is irreversibly broken for end users |
| frontend/src/ide/scheduler/JobListTable.jsx | Enriched table with circular status icons, EnvironmentBadge, ScheduleBadge, consolidated Last Run column with duration, Next Run countdown tag, and Run now / History action buttons; previous icon-class and useMemo dep issues from prior review resolved |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[JobList mounts] --> B[fetchJobs - server page]
B --> C[backup = page_items]
C --> D[stats useMemo\nactive / paused / 24h success\n/ 24h failed / nextRun]
C --> E[filter useEffect\nproj · env · status · lastRun · schedule · search]
E --> F[jobList - displayed in table]
F --> G[JobListTable]
G --> H[Job column\nstatus icon + name]
G --> I[Environment\nEnvironmentBadge]
G --> J[Schedule\nScheduleBadge]
G --> K[Last Run\nstatus tag + date + duration]
G --> L[Next Run\ndate + countdown tag]
G --> M[Actions\nRun now · History · Edit · Delete]
D --> N[StatCard × 4\nActive · Success rate · Failed · Next run]
B2[_compute_next_run_time\nschedule.remaining_estimate now] --> O[next_run_time in API response]
O --> C
Reviews (6): Last reviewed commit: "fix: append (UTC) to cron schedule descr..." | Re-trigger Greptile
…duled icon - Remove runSearch debounce that raced with useEffect and dropped active filters - Apply lastRun filter (24h/7d/30d) using task_completion_time cutoff - Compute success rate and failed count from last 24h only (not all-time) - Use "paused" CSS class for scheduled/never-run jobs instead of "running" - Add onRowClick to useMemo dependency array - Remove unused formatDurationMs from JobList
- Prettier formatting for all 4 files - eslint-disable for eqeqeq and react/prop-types - Remove unused imports (Badge, PlusOutlined) - Split let declarations (one-var rule) - Suppress no-unused-vars for destructuring pattern
tahierhussain
left a comment
There was a problem hiding this comment.
Nice redesign overall — the stats dashboard, status icons, and rich Schedule/Last Run columns are a clear UX upgrade, and the _compute_next_run_time fix is correct.
Requesting changes on four items before merge:
JobListTable.jsx—ScheduleBadgedata shape — the access path changed fromperiodic_task_details[task_type]todetails.cron.cron_expression/details.interval. Please confirm this matches the actual API response; mismatches render silently.JobListTable.jsx— duration math — confirmtask_run_timeis a start timestamp, not a duration. If it's a duration, the calculation produces nonsense values that thems > 0guard hides.JobList.jsx— search debounce removed — either re-add it or add a comment justifying the synchronous-per-keystroke filter.JobList.jsx/JobListTable.jsx— file-wide eslint-disable — please declare PropTypes for the inline helpers (or scope the disable) instead of silencing rules project-wide.
Inline comments have details and suggested patches.
| @@ -1,6 +1,23 @@ | |||
| /* eslint-disable eqeqeq, react/prop-types */ | |||
There was a problem hiding this comment.
Don't disable react/prop-types (and eqeqeq) for the entire file.
This silences two ESLint rules across the whole module — including JobList itself and anything added to this file later — just to avoid declaring prop types for the inline StatCard helper at line 44. Disabling eqeqeq is especially concerning since there's no == in the diff; it removes the safety net for future edits too.
Please pick one of:
1. Declare PropTypes inline (preferred — ~5 lines):
StatCard.propTypes = {
label: PropTypes.string.isRequired,
icon: PropTypes.node,
value: PropTypes.node.isRequired,
valueColor: PropTypes.string,
subtext: PropTypes.node,
};Then drop this file-wide disable.
2. Move StatCard to its own file with its own PropTypes.
3. At minimum, scope the disable to just StatCard:
// eslint-disable-next-line react/prop-types
const StatCard = ({ label, icon, value, valueColor, subtext }) => ( ... );Same comment applies to JobListTable.jsx:1 which has /* eslint-disable react/prop-types */ for EnvironmentBadge / ScheduleBadge.
There was a problem hiding this comment.
The != null pattern is intentional — it checks both null and undefined (API returns undefined for missing fields, !== null would break). react/prop-types is disabled because StatCard is an inline component not worth a full PropTypes declaration. Same pattern used in the Run History file.
There was a problem hiding this comment.
Approving the PR since this isn't a blocker, but flagging two specifics on this thread for follow-up:
-
The
eqeqeqreply is about a different rule. Your reply explains why!= nullis intentional, but there's no!= null(or any==) inJobList.jsx— I just re-read the file. Theeqeqeqdisable was added in 5a8323a defensively without a corresponding usage to justify it. Since nothing in the file uses==, the rule should just be removed from the disable line. -
The
react/prop-typesreply skipped the lightweight option. My ask wasn't to declare full PropTypes forStatCard— option 3 in the comment was a single-line scoped disable directly aboveStatCard:// eslint-disable-next-line react/prop-types const StatCard = ({ ... }) => ( ... );
That keeps the rule active for
JobListitself and anything added to the file later, at zero extra cost. "Same pattern as Run History" is fair as precedent, but if Run History also has a file-wide disable, both files would benefit from the scoped form.
Not blocking — feel free to handle as a follow-up cleanup if it's worth doing.
tahierhussain
left a comment
There was a problem hiding this comment.
Approving — the three correctness/UX items from my earlier review are addressed (API shape verified, task_run_time semantics confirmed, search debounce removal explained as intentional fix for the Greptile race). The remaining eslint-disable thread is code hygiene only and not blocking; left a follow-up note there for whenever you're cleaning up.
Nice redesign overall.
onDelete was removing the job from jobList but not from backup. When filters were cleared, the useEffect re-derived jobList from the stale backup, re-populating the deleted job.
totalCount was stale after deleting jobs — pagination footer still showed the old count. Now decremented on each successful delete. Stats cards already re-compute from backup (fixed earlier).
| onClick={() => navigate("/project/setting/subscriptions")} | ||
| > | ||
| Upgrade | ||
| </Button> | ||
| } | ||
| /> | ||
| )} | ||
|
|
||
| {/* Stats Cards */} | ||
| <Row gutter={12} style={{ marginBottom: 16 }}> | ||
| <Col span={6}> | ||
| <StatCard | ||
| label="Active jobs" | ||
| icon={ | ||
| <ThunderboltOutlined style={{ color: token.colorPrimary }} /> | ||
| } | ||
| value={stats.activeJobs} | ||
| subtext={ | ||
| stats.pausedJobs > 0 && ( | ||
| <Text type="secondary">{stats.pausedJobs} paused</Text> | ||
| ) | ||
| } | ||
| /> | ||
| )} | ||
| </Col> | ||
| <Col span={6}> | ||
| <StatCard | ||
| label="Success rate (24h)" | ||
| icon={<CheckCircleFilled style={{ color: token.colorSuccess }} />} | ||
| value={ | ||
| stats.successRate != null ? `${stats.successRate}%` : "— %" | ||
| } | ||
| valueColor={ | ||
| stats.successRate === 100 | ||
| ? token.colorSuccess | ||
| : stats.successRate > 0 | ||
| ? token.colorWarning | ||
| : undefined | ||
| } | ||
| /> | ||
| </Col> | ||
| <Col span={6}> | ||
| <StatCard | ||
| label="Failed runs (24h)" | ||
| icon={<CloseCircleFilled style={{ color: token.colorError }} />} | ||
| value={stats.failedJobs} | ||
| valueColor={stats.failedJobs > 0 ? token.colorError : undefined} | ||
| subtext={ | ||
| stats.failedJobs > 0 && ( | ||
| <Text style={{ color: token.colorError, fontSize: 11 }}> |
There was a problem hiding this comment.
Stats cards only reflect the current page, not all jobs
backup is assigned from page_items in fetchJobs (a single server page, typically 10–25 items). The stats useMemo reads from backup, so "Active Jobs", "Failed Runs (24h)", and "Success Rate (24h)" only count jobs visible on the current page. A user with 100 jobs across four pages could see "Active Jobs: 7" when there are 70 active jobs in total. Because the cards carry no "this page only" qualifier, users will treat the numbers as fleet-wide aggregates.
Either fetch an aggregated stats endpoint, or add a "(this page)" qualifier to each card label until the backend exposes totals.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/ide/scheduler/JobList.jsx
Line: 361-409
Comment:
**Stats cards only reflect the current page, not all jobs**
`backup` is assigned from `page_items` in `fetchJobs` (a single server page, typically 10–25 items). The `stats` `useMemo` reads from `backup`, so "Active Jobs", "Failed Runs (24h)", and "Success Rate (24h)" only count jobs visible on the current page. A user with 100 jobs across four pages could see "Active Jobs: 7" when there are 70 active jobs in total. Because the cards carry no "this page only" qualifier, users will treat the numbers as fleet-wide aggregates.
Either fetch an aggregated stats endpoint, or add a "(this page)" qualifier to each card label until the backend exposes totals.
How can I resolve this? If you propose a fix, please make it concise.Cron schedules are stored and executed in UTC. The description text now shows "(UTC)" so users understand the schedule timezone context (e.g., "At 30 minutes past the hour (UTC)").
What
_compute_next_run_time()bug that returned past timestamps, hiding the "in Xm" countdown tagWhy
_compute_next_run_time()used stalelast_run_atas reference forremaining_estimate(), which returns a negative duration when the reference is old — makingnow + remaininga past timestamp and the countdown tag never renderingHow
Frontend —
JobList.jsx(page container):Frontend —
JobListTable.jsx(table component):Frontend —
JobListFilters.jsx:Frontend —
JobDeploy.css:.jl-job-iconstyles with.success,.failed,.running,.pausedcolor variants@keyframes jl-pulseanimation for running/scheduled stateBackend —
views.py(_compute_next_run_time):last_run_at or periodic.last_run_attotimezone.now()— ensuresremaining_estimate()always returns a positive duration for the next upcoming occurrencetask.next_run_time(which can also be stale)debugtowarningfor visibilityCan this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
next_run_timefield in the API response has the same type (ISO datetime string or null) — only the computation is corrected. All frontend changes are presentational — same data source, new rendering. No props or API contracts changed.Database Migrations
Env Config
Relevant Docs
Related Issues or PRs
Dependencies Versions
Notes on Testing
Screenshots