From bcd79a1b3ea10c3e8b75812ad74b09ef1543df75 Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Wed, 15 Apr 2026 19:10:50 +0530 Subject: [PATCH 1/8] 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=, 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= 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) --- backend/backend/core/scheduler/views.py | 1 + frontend/src/common/helpers.js | 35 +++-- frontend/src/ide/run-history/Runhistory.jsx | 32 ++++- frontend/src/ide/scheduler/JobListTable.jsx | 139 ++++++++++++++------ 4 files changed, 152 insertions(+), 55 deletions(-) diff --git a/backend/backend/core/scheduler/views.py b/backend/backend/core/scheduler/views.py index 122ff150..9857c5f5 100644 --- a/backend/backend/core/scheduler/views.py +++ b/backend/backend/core/scheduler/views.py @@ -164,6 +164,7 @@ def _serialize_task(task): "task_status": task.status, "task_run_time": task.task_run_time, "task_completion_time": task.task_completion_time, + "next_run_time": task.next_run_time, "task_type": task_type, "description": task.description, "environment": { diff --git a/frontend/src/common/helpers.js b/frontend/src/common/helpers.js index 83a1afe6..8d00ccbe 100644 --- a/frontend/src/common/helpers.js +++ b/frontend/src/common/helpers.js @@ -354,15 +354,31 @@ const getRelativeTime = (dateString) => { const now = new Date(); const then = new Date(dateString); const diffMs = now - then; - const diffMins = Math.floor(diffMs / 60000); - if (diffMins < 1) return "just now"; - if (diffMins < 60) return `${diffMins}m ago`; - const diffHrs = Math.floor(diffMins / 60); - if (diffHrs < 24) return `${diffHrs}h ago`; - const diffDays = Math.floor(diffHrs / 24); - if (diffDays < 30) return `${diffDays}d ago`; - const diffMonths = Math.floor(diffDays / 30); - return `${diffMonths}mo ago`; + const isPast = diffMs >= 0; + const absMs = Math.abs(diffMs); + const mins = Math.floor(absMs / 60000); + const fmt = (n, unit) => (isPast ? `${n}${unit} ago` : `in ${n}${unit}`); + if (mins < 1) return isPast ? "just now" : "in a moment"; + if (mins < 60) return fmt(mins, "m"); + const hrs = Math.floor(mins / 60); + if (hrs < 24) return fmt(hrs, "h"); + const days = Math.floor(hrs / 24); + if (days < 30) return fmt(days, "d"); + const months = Math.floor(days / 30); + return fmt(months, "mo"); +}; + +const formatDateTime = (dateString) => { + if (!dateString) return ""; + const d = new Date(dateString); + if (Number.isNaN(d.getTime())) return ""; + return d.toLocaleString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + }); }; export { @@ -388,4 +404,5 @@ export { extractFormulaExpression, validateFormulaExpression, getRelativeTime, + formatDateTime, }; diff --git a/frontend/src/ide/run-history/Runhistory.jsx b/frontend/src/ide/run-history/Runhistory.jsx index 310b09f0..ae3b190a 100644 --- a/frontend/src/ide/run-history/Runhistory.jsx +++ b/frontend/src/ide/run-history/Runhistory.jsx @@ -17,6 +17,7 @@ import { DatabaseOutlined, CloseCircleFilled, } from "@ant-design/icons"; +import { useSearchParams } from "react-router-dom"; import { useAxiosPrivate } from "../../service/axios-service"; import { orgStore } from "../../store/org-store"; @@ -102,6 +103,7 @@ const Runhistory = () => { const { selectedOrgId } = orgStore(); const { token } = theme.useToken(); const { notify } = useNotificationService(); + const [searchParams, setSearchParams] = useSearchParams(); /* ─── API calls ─── */ const getRunHistoryList = async ( @@ -153,8 +155,14 @@ const Runhistory = () => { setJobSchedule(scheduledObj); setJobListItems(jobIds); if (jobIds.length) { - setFilterQuery((prev) => ({ ...prev, job: jobIds[0].value })); - getRunHistoryList(jobIds[0].value); + const taskFromUrl = searchParams.get("task"); + const taskFromUrlNum = taskFromUrl ? Number(taskFromUrl) : NaN; + const matchedFromUrl = !Number.isNaN(taskFromUrlNum) + ? jobIds.find((j) => j.value === taskFromUrlNum) + : null; + const initial = matchedFromUrl?.value ?? jobIds[0].value; + setFilterQuery((prev) => ({ ...prev, job: initial })); + getRunHistoryList(initial); } } catch (error) { console.error("Failed to load jobs", error); @@ -200,10 +208,22 @@ const Runhistory = () => { }, [backUpData]); /* ─── handlers ─── */ - const handleJobChange = useCallback((value) => { - setFilterQuery({ status: "", job: value, trigger: "", scope: "" }); - getRunHistoryList(value); - }, []); + const handleJobChange = useCallback( + (value) => { + setFilterQuery({ status: "", job: value, trigger: "", scope: "" }); + getRunHistoryList(value); + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + if (value) next.set("task", String(value)); + else next.delete("task"); + return next; + }, + { replace: true } + ); + }, + [setSearchParams, getRunHistoryList] + ); const handleTriggerChange = useCallback((value) => { setFilterQuery((prev) => ({ ...prev, trigger: value || "" })); diff --git a/frontend/src/ide/scheduler/JobListTable.jsx b/frontend/src/ide/scheduler/JobListTable.jsx index 784fa88b..694e0e23 100644 --- a/frontend/src/ide/scheduler/JobListTable.jsx +++ b/frontend/src/ide/scheduler/JobListTable.jsx @@ -16,11 +16,17 @@ import { DeleteOutlined, PlayCircleOutlined, LoadingOutlined, + HistoryOutlined, } from "@ant-design/icons"; import PropTypes from "prop-types"; +import { useNavigate } from "react-router-dom"; import { useJobService } from "./service"; -import { getTooltipText } from "../../common/helpers"; +import { + getTooltipText, + getRelativeTime, + formatDateTime, +} from "../../common/helpers"; import { useNotificationService } from "../../service/notification-service"; const JobListTable = memo( @@ -35,6 +41,28 @@ const JobListTable = memo( const { updateTask, runTask } = useJobService(); const [loading, setLoading] = useState({}); const { notify } = useNotificationService(); + const navigate = useNavigate(); + + const goToRunHistory = (userTaskId) => + navigate(`/project/job/history?task=${userTaskId}`); + + const renderDateCell = (text) => { + if (!text) { + return ( + Not started yet. + ); + } + return ( + + + {formatDateTime(text)} + + {getRelativeTime(text)} + + + + ); + }; const handleSwitchSchedular = async (item, checked) => { try { const { @@ -97,10 +125,18 @@ const JobListTable = memo( title: "Job", dataIndex: "task_name", key: "task_name", - render: (text) => ( - - {text} - + render: (text, record) => ( + + + ), }, { @@ -124,53 +160,76 @@ const JobListTable = memo( ), }, { - title: "Schedule Type", + title: "Schedule", key: "schedule", - render: (_, record) => ( - - {getTooltipText( - record.periodic_task_details?.[record.task_type] ?? {}, - record.task_type - )} - {record.task_status === "FAILED" && ( - <> -
- Logs available in Run History - - )} - - } - > - { + const scheduleText = getTooltipText( + record.periodic_task_details?.[record.task_type] ?? {}, + record.task_type + ); + return ( + + }> + {record.task_type === "interval" ? "Interval" : "Cron"} + + + {scheduleText} + + + ); + }, + }, + { + title: "Last Run Status", + key: "last_run_status", + render: (_, record) => { + if (!record.task_status) { + return ; + } + const isFailed = [ + "FAILED", + "FAILED PERMANENTLY", + "FAILURE", + ].includes(record.task_status); + return ( + } > - {record.task_status} - -
- ), + + {record.task_status === "FAILURE" + ? "FAILED" + : record.task_status} + + + ); + }, }, { title: "Last Run", dataIndex: "task_completion_time", key: "last_run", - render: (text) => ( - {text || "Not started yet."} - ), + render: renderDateCell, }, { title: "Next Run", - dataIndex: "task_run_time", + dataIndex: "next_run_time", key: "next_run", - render: (text) => ( - {text || "Not started yet."} - ), + render: renderDateCell, }, { title: "Status", From 2b89fc7ef9ab11084ca475ae57c453f76ac4200e Mon Sep 17 00:00:00 2001 From: abhizipstack Date: Wed, 15 Apr 2026 19:15:07 +0530 Subject: [PATCH 2/8] 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) --- frontend/src/ide/scheduler/JobDeploy.jsx | 33 +++++++- .../src/ide/scheduler/ModelConfigsTable.jsx | 83 ++++++++++++++++--- 2 files changed, 104 insertions(+), 12 deletions(-) diff --git a/frontend/src/ide/scheduler/JobDeploy.jsx b/frontend/src/ide/scheduler/JobDeploy.jsx index 5c28c079..cfd111a6 100644 --- a/frontend/src/ide/scheduler/JobDeploy.jsx +++ b/frontend/src/ide/scheduler/JobDeploy.jsx @@ -21,6 +21,7 @@ import { Divider, Spin, Collapse, + Tooltip, } from "antd"; import { ClockCircleOutlined, @@ -32,6 +33,8 @@ import { LinkOutlined, ExpandAltOutlined, ShrinkOutlined, + PlusOutlined, + ReloadOutlined, } from "@ant-design/icons"; import { checkPermission } from "../../common/helpers"; @@ -459,7 +462,35 @@ const JobDeploy = memo(function JobDeploy({ + Environment + + +