{
const kw = row?.kwargs || {};
const legacyQuick = kw.source === "quick_deploy";
const models = kw.models_override || [];
- const trigger = kw.trigger || (legacyQuick ? "manual" : "scheduled");
+ const trigger =
+ row?.trigger || kw.trigger || (legacyQuick ? "manual" : "scheduled");
const scope =
- kw.scope || (models.length > 0 || legacyQuick ? "model" : "job");
+ row?.scope ||
+ kw.scope ||
+ (models.length > 0 || legacyQuick ? "model" : "job");
return { trigger, scope, models };
};
@@ -114,15 +117,19 @@ const Runhistory = () => {
/* ─── API calls ─── */
const getRunHistoryList = useCallback(
- async (Id, page = currentPage, limit = pageSize) => {
+ async (Id, page = currentPage, limit = pageSize, filters = {}) => {
setLoading(true);
try {
+ const params = { page, limit };
+ if (filters.status) params.status = filters.status;
+ if (filters.trigger) params.trigger = filters.trigger;
+ if (filters.scope) params.scope = filters.scope;
const res = await axios({
method: "GET",
url: `/api/v1/visitran/${
selectedOrgId || "default_org"
}/project/_all/jobs/run-history/${Id}`,
- params: { page, limit },
+ params,
});
const { page_items, total_items, current_page } = res.data.data;
setTotalCount(total_items);
@@ -137,7 +144,7 @@ const Runhistory = () => {
setLoading(false);
}
},
- [axios, selectedOrgId, currentPage, pageSize, notify]
+ [axios, selectedOrgId, notify]
);
const getJobList = async () => {
@@ -171,7 +178,6 @@ const Runhistory = () => {
: null;
const initial = matchedFromUrl?.value ?? jobIds[0].value;
setFilterQuery((prev) => ({ ...prev, job: initial }));
- getRunHistoryList(initial, 1, pageSize);
}
} catch (error) {
console.error("Failed to load jobs", error);
@@ -184,44 +190,46 @@ const Runhistory = () => {
getJobList();
}, []);
- /* ─── client-side status + trigger + scope filters ─── */
+ const deepLinkConsumed = useRef(false);
+
+ /* ─── server-side filtering: refetch when filters change ─── */
useEffect(() => {
- let filtered = backUpData;
- if (filterQueries.status) {
- filtered = filtered.filter((el) => el.status === filterQueries.status);
- }
- if (filterQueries.trigger) {
- filtered = filtered.filter(
- (el) => getRunTriggerScope(el).trigger === filterQueries.trigger
- );
- }
- if (filterQueries.scope) {
- filtered = filtered.filter(
- (el) => getRunTriggerScope(el).scope === filterQueries.scope
- );
- }
- setJobHistoryData(filtered);
+ if (!filterQueries.job) return;
+ getRunHistoryList(filterQueries.job, 1, pageSize, {
+ status: filterQueries.status,
+ trigger: filterQueries.trigger,
+ scope: filterQueries.scope,
+ });
}, [
filterQueries.status,
filterQueries.trigger,
filterQueries.scope,
- backUpData,
+ filterQueries.job,
]);
- /* ─── auto-expand failed rows on fresh data load (not on filter changes) ─── */
+ /* ─── auto-expand on fresh data load ─── */
useEffect(() => {
- const failedIds = (backUpData || [])
+ const ids = [];
+ if (
+ !deepLinkConsumed.current &&
+ searchParams.has("task") &&
+ backUpData.length > 0
+ ) {
+ ids.push(backUpData[0].id);
+ deepLinkConsumed.current = true;
+ }
+ (backUpData || [])
.filter((r) => r.status === "FAILURE" && r.error_message)
- .map((r) => r.id);
- setExpandedRowKeys(failedIds);
+ .forEach((r) => {
+ if (!ids.includes(r.id)) ids.push(r.id);
+ });
+ setExpandedRowKeys(ids);
}, [backUpData]);
/* ─── handlers ─── */
const handleJobChange = useCallback(
(value) => {
setFilterQuery({ status: "", job: value, trigger: "", scope: "" });
- setCurrentPage(1);
- getRunHistoryList(value, 1, pageSize);
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
@@ -232,7 +240,7 @@ const Runhistory = () => {
{ replace: true }
);
},
- [setSearchParams, getRunHistoryList]
+ [setSearchParams]
);
const handleTriggerChange = useCallback((value) => {
@@ -249,19 +257,27 @@ const Runhistory = () => {
const handleRefresh = useCallback(() => {
if (filterQueries.job) {
- getRunHistoryList(filterQueries.job);
+ getRunHistoryList(filterQueries.job, currentPage, pageSize, {
+ status: filterQueries.status,
+ trigger: filterQueries.trigger,
+ scope: filterQueries.scope,
+ });
}
- }, [filterQueries.job]);
+ }, [filterQueries, currentPage, pageSize, getRunHistoryList]);
const handlePagination = useCallback(
(newPage, newPageSize) => {
if (currentPage !== newPage || pageSize !== newPageSize) {
setCurrentPage(newPage);
setPageSize(newPageSize);
- getRunHistoryList(envInfo.id, newPage, newPageSize);
+ getRunHistoryList(filterQueries.job, newPage, newPageSize, {
+ status: filterQueries.status,
+ trigger: filterQueries.trigger,
+ scope: filterQueries.scope,
+ });
}
},
- [currentPage, pageSize, envInfo.id]
+ [currentPage, pageSize, filterQueries, getRunHistoryList]
);
const handleExpand = useCallback((expanded, record) => {
@@ -591,6 +607,37 @@ const Runhistory = () => {
: "No model configuration recorded for this run."}
+ {record.result?.total > 0 && (
+
+
+ {record.result.total || 0} models
+ attempted
+
+
+ {record.result.passed || 0} passed
+
+
+ {record.result.failed || 0} failed
+
+ {record.result.models?.length > 0 && (
+
+ {record.result.models
+ .map((m) => `${m.name} (${m.end_status})`)
+ .join(", ")}
+
+ )}
+
+ )}
{isFailure && record.error_message && (
{
+ if (prefillModel && prev[prefillModel]) {
+ return { [prefillModel]: prev[prefillModel] };
+ }
+ return {};
+ });
}
})
.catch((err) => {
console.error("Failed to load models", err);
})
.finally(() => setModelsLoading(false));
- }, [open, selectedProjectId, isEditMode]);
+ }, [open, selectedProjectId, isEditMode, prefillModel]);
/* ─── auto-open Model Configuration when project is selected ─── */
useEffect(() => {
@@ -223,6 +232,29 @@ const JobDeploy = memo(function JobDeploy({
}
}, [selectedProjectId]);
+ /* ─── pre-fill project from Quick Deploy CTA ─── */
+ useEffect(() => {
+ if (!open || !prefillProject || isEditMode) return;
+ form.setFieldsValue({ project: prefillProject });
+ setSelectedProjectId(prefillProject);
+ }, [open, prefillProject, isEditMode, form]);
+
+ /* ─── pre-fill model from Quick Deploy CTA ─── */
+ useEffect(() => {
+ if (!open || !prefillModel || isEditMode) return;
+ setModelConfigs((prev) => ({
+ ...prev,
+ [prefillModel]: {
+ ...prev[prefillModel],
+ enabled: true,
+ materialization: prev[prefillModel]?.materialization || "TABLE",
+ },
+ }));
+ setModelConfigActiveKey((prev) =>
+ prev.includes("model-config") ? prev : ["model-config"]
+ );
+ }, [open, prefillModel, isEditMode]);
+
/* ─── load existing job when editing ─── */
useEffect(() => {
if (!open || !selectedJobDeployId) return;
@@ -779,6 +811,8 @@ JobDeploy.propTypes = {
PropTypes.number,
]),
setIsJobListModified: PropTypes.func.isRequired,
+ prefillModel: PropTypes.string,
+ prefillProject: PropTypes.string,
};
JobDeploy.displayName = "JobDeploy";
diff --git a/frontend/src/ide/scheduler/JobList.jsx b/frontend/src/ide/scheduler/JobList.jsx
index ca0a521..7bca882 100644
--- a/frontend/src/ide/scheduler/JobList.jsx
+++ b/frontend/src/ide/scheduler/JobList.jsx
@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback, useMemo } from "react";
import { Alert, Button, Space, Typography, Modal, Pagination } from "antd";
import debounce from "lodash/debounce";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useSearchParams } from "react-router-dom";
import { checkPermission } from "../../common/helpers";
import { useNotificationService } from "../../service/notification-service";
@@ -38,6 +38,9 @@ const JobList = () => {
const [openJobDeploy, setOpenJobDeploy] = useState(false);
const [selectedJobId, setSelectedJobId] = useState(null);
const [searchQuery, setSearchQuery] = useState("");
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [prefillModel, setPrefillModel] = useState(null);
+ const [prefillProject, setPrefillProject] = useState(null);
const [filters, setFilters] = useState({ proj: "all", env: "all" });
const {
currentPage,
@@ -154,9 +157,22 @@ const JobList = () => {
}, []);
useEffect(() => {
- if (!openJobDeploy) setSelectedJobId(null);
+ if (!openJobDeploy) {
+ setSelectedJobId(null);
+ setPrefillModel(null);
+ setPrefillProject(null);
+ }
}, [openJobDeploy]);
+ useEffect(() => {
+ if (searchParams.get("create") === "1") {
+ setPrefillModel(searchParams.get("model") || null);
+ setPrefillProject(searchParams.get("project") || null);
+ setOpenJobDeploy(true);
+ setSearchParams({}, { replace: true });
+ }
+ }, [searchParams, setSearchParams]);
+
const onDelete = async () => {
try {
await deleteTask(delTaskDetail.projectId, delTaskDetail.taskId);
@@ -259,6 +275,8 @@ const JobList = () => {
setOpen={setOpenJobDeploy}
selectedJobDeployId={selectedJobId}
setIsJobListModified={setIsJobListModified}
+ prefillModel={prefillModel}
+ prefillProject={prefillProject}
/>
{
+ const url = `${jobsUrl(projId)}/run-history/${taskId}`;
+ const response = await axiosPrivate.get(url, {
+ params: { page: 1, limit: 1 },
+ });
+ const runs = response.data?.data?.page_items?.run_history || [];
+ return runs.length > 0 ? runs[0] : null;
+ };
+
const listRecentRunsForModel = async (projId, modelName, limit = 5) => {
const url = `${jobsUrl(
projId
@@ -144,6 +153,7 @@ export function useJobService() {
runTaskForModel,
listDeployCandidates,
listRecentRunsForModel,
+ getLatestRunStatus,
getProjects,
getEnvironments,
getProjectModels,