Skip to content

Commit f4e9081

Browse files
abhizipstackclaude
andauthored
feat: Deploy, Jobs & Run History UX improvements (#61)
* feat(jobs-list): clickable job names, split columns, human dates - Job name cell is now a History-icon link that navigates to /project/job/history?task=<user_task_id>, preselecting that job's run history. - "Schedule Type" previously rendered task_status (SUCCESS / FAILED) which conflated schedule-type with last-run-status. Renamed to "Schedule" and renders the actual schedule ("every 1 hours" / cron expression) behind a cron-vs-interval tag. Added a new "Last Run Status" column for the run-status part. - Last Run / Next Run columns render a local-formatted datetime plus a muted relative time underneath; native title holds the ISO. Next Run now reads from next_run_time (new field on the API) rather than task_run_time which was the last-start time mistakenly shown as "Next Run". - Run History page accepts ?task=<id> on mount and writes the param back when the user changes the selector, so deep-links are shareable. - helpers.js: getRelativeTime handles future dates ("in 2h") and a new formatDateTime helper returns "Oct 14, 2026, 3:25 PM" style. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(job-form): richer materialization help + quick Env creation link - Materialization dropdown options now include detailed descriptions (when to pick each, implications) rendered inline in the popup, not just a single-line hint. Column header gains an info tooltip with a TABLE / VIEW / INCREMENTAL summary plus a note on switching materialization later. - Environment field gets a "Create new" link (opens /project/env/list in a new tab so unsaved job-form state isn't lost) and a refresh icon button to reload the env list after creation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(job-form): Refresh button on incremental config no longer no-ops The Refresh button on an expanded Incremental model panel appeared to do nothing useful — dropdowns wouldn't repopulate. Root cause was a stale-closure race: setColumnCache((prev) => { delete entry; return rest; }) fetchColumnsForModel(modelName) The fetchColumnsForModel reference captured by onClick had the pre-deletion columnCache in its closure, so its early-return "if (columnCache[modelName]) return" fired against the old snapshot and the refetch never ran. The cache got cleared but never refilled. Added a { force: true } option to fetchColumnsForModel that bypasses the cache check. Refresh now calls it with force=true and a single network round-trip repopulates the column caches. Avoids the setState + stale-closure interaction entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(run-history): per-run insights on expanded row Expandable rows now cover more than just FAILURE. Clicking the expander on any SUCCESS / FAILURE / RETRY / REVOKED run opens an insights panel that reads as an extension of the row: - Status header with a tinted icon (green check for success, red cross for failure, etc.), start_time, and duration. - Scope tag (Single model vs Full job) plus the list of models attempted. Model list is derived from kwargs.models_override for scoped runs or from kwargs.model_configs.enabled for full runs. Back-compat handling for legacy kwargs.source === "quick_deploy". - For FAILURE runs, the error_message renders in a preformatted, scrollable Alert so stack traces preserve whitespace and don't blow out the page. - Whole panel uses theme tokens (colorSuccessBg / colorErrorBg / colorInfoBg etc.) so light and dark both track. Runtime-captured metrics (rows processed, tables written, schemas touched) are not yet surfaced because the scheduler pipeline doesn't persist them to TaskRunHistory.result today. Documented in the PR for follow-up instrumentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(run-history): human-readable Triggered column + insights header The Triggered column was rendering the raw ISO timestamp straight from the API, which is hard to read at a glance. Now the cell shows a locale-formatted date ("Oct 14, 2026, 3:25 PM") with a muted relative time below ("2h ago" / "in 3h"); the full ISO sits in a native tooltip for machine-precise inspection. The expanded insights panel's header reuses the same formatting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — compute next_run_time + memoize STATUS_META P1: next_run_time was exposed in the API but never written, so the "Next Run" column was always empty. Added _compute_next_run_time that uses celery's remaining_estimate on the PeriodicTask's schedule to derive the next run from the last run time. Falls back to task.next_run_time if it happens to be set directly. P2: STATUS_META was rebuilt on every render inside the component body. Wrapped in useMemo with [token] as the dependency so it only recomputes when the antd theme changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address reviewer feedback — next_run_time, null guards, stale closure P1: _compute_next_run_time added remaining to reference (last_run_at) instead of now, giving wrong results for interval jobs. Fixed to use timezone.now() + remaining. Added logger.debug on failure. P1: getTooltipText crashed when a job had no linked periodic task. Guarded with optional chaining on periodic_task_details. P2: Auto-expand useEffect used JobHistoryData as dep, resetting user-expanded rows on every filter change. Switched to backUpData so it only fires on fresh data loads. P2: handleJobChange useCallback was missing getRunHistoryList in its dependency array, risking stale closure. Added to deps. Nit: Added RUNNING key to STATUS_META so backends that send RUNNING instead of STARTED render correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: wrap getRunHistoryList in useCallback for stable reference getRunHistoryList was defined directly in the component body, causing handleJobChange's useCallback to recreate on every render since its dependency changed each time. Wrapped in useCallback with explicit deps so handleJobChange gets a stable reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 73ead92 commit f4e9081

6 files changed

Lines changed: 469 additions & 153 deletions

File tree

backend/backend/core/scheduler/views.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@
2020
logger = logging.getLogger(__name__)
2121

2222

23+
24+
def _compute_next_run_time(periodic, last_run_at):
25+
"""Derive the next run time from a PeriodicTask's schedule."""
26+
if not periodic or not periodic.enabled:
27+
return None
28+
try:
29+
schedule = periodic.schedule
30+
reference = last_run_at or periodic.last_run_at or timezone.now()
31+
remaining = schedule.remaining_estimate(reference)
32+
return timezone.now() + remaining
33+
except Exception:
34+
logger.debug("Failed to compute next_run_time for %s", periodic, exc_info=True)
35+
return None
36+
37+
2338
def _is_valid_project_id(project_id):
2439
"""Check if project_id is a real UUID (not a placeholder like '_all' or 'all')."""
2540
try:
@@ -164,6 +179,9 @@ def _serialize_task(task):
164179
"task_status": task.status,
165180
"task_run_time": task.task_run_time,
166181
"task_completion_time": task.task_completion_time,
182+
"next_run_time": task.next_run_time or _compute_next_run_time(
183+
periodic, task.task_run_time
184+
),
167185
"task_type": task_type,
168186
"description": task.description,
169187
"environment": {

frontend/src/common/helpers.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -354,15 +354,31 @@ const getRelativeTime = (dateString) => {
354354
const now = new Date();
355355
const then = new Date(dateString);
356356
const diffMs = now - then;
357-
const diffMins = Math.floor(diffMs / 60000);
358-
if (diffMins < 1) return "just now";
359-
if (diffMins < 60) return `${diffMins}m ago`;
360-
const diffHrs = Math.floor(diffMins / 60);
361-
if (diffHrs < 24) return `${diffHrs}h ago`;
362-
const diffDays = Math.floor(diffHrs / 24);
363-
if (diffDays < 30) return `${diffDays}d ago`;
364-
const diffMonths = Math.floor(diffDays / 30);
365-
return `${diffMonths}mo ago`;
357+
const isPast = diffMs >= 0;
358+
const absMs = Math.abs(diffMs);
359+
const mins = Math.floor(absMs / 60000);
360+
const fmt = (n, unit) => (isPast ? `${n}${unit} ago` : `in ${n}${unit}`);
361+
if (mins < 1) return isPast ? "just now" : "in a moment";
362+
if (mins < 60) return fmt(mins, "m");
363+
const hrs = Math.floor(mins / 60);
364+
if (hrs < 24) return fmt(hrs, "h");
365+
const days = Math.floor(hrs / 24);
366+
if (days < 30) return fmt(days, "d");
367+
const months = Math.floor(days / 30);
368+
return fmt(months, "mo");
369+
};
370+
371+
const formatDateTime = (dateString) => {
372+
if (!dateString) return "";
373+
const d = new Date(dateString);
374+
if (Number.isNaN(d.getTime())) return "";
375+
return d.toLocaleString(undefined, {
376+
month: "short",
377+
day: "numeric",
378+
year: "numeric",
379+
hour: "numeric",
380+
minute: "2-digit",
381+
});
366382
};
367383

368384
export {
@@ -388,4 +404,5 @@ export {
388404
extractFormulaExpression,
389405
validateFormulaExpression,
390406
getRelativeTime,
407+
formatDateTime,
391408
};

0 commit comments

Comments
 (0)