From 849a573915777164c54ba8d5233a15a6138654e4 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Tue, 7 Oct 2025 21:22:57 +0200 Subject: [PATCH 01/11] centralized logic job status handling --- node/src/components/data/JobRow.tsx | 32 +++------- node/src/components/data/JobSelector.tsx | 40 +++--------- node/src/utils/function_utils.ts | 80 ++++++++++++++++++++---- 3 files changed, 84 insertions(+), 68 deletions(-) diff --git a/node/src/components/data/JobRow.tsx b/node/src/components/data/JobRow.tsx index 5918aca9..78f82192 100644 --- a/node/src/components/data/JobRow.tsx +++ b/node/src/components/data/JobRow.tsx @@ -5,7 +5,7 @@ import Typography from "@mui/material/Typography"; import { useState } from "react"; import { toast } from "react-toastify"; import { Function as OsparcFunction } from "../../osparc-api-ts-client"; -import { createJobStudyCopy, openStudyUid } from "../../utils/function_utils"; +import { createJobStudyCopy, extractJobStatus, openStudyUid } from "../../utils/function_utils"; import CustomTooltip from "../utils/CustomTooltip"; interface JobRowProps { @@ -91,7 +91,7 @@ function JobRow(props: JobRowProps) { ); } - const jobStatus = job.job.status; + const jobStatus = extractJobStatus(job); let outputs; if (jobStatus === "SUCCESS") { outputs = Object.entries(job.job.outputs).map(([key, value]) => ( @@ -100,32 +100,16 @@ function JobRow(props: JobRowProps) { {", "} )); - } else if (jobStatus === "STARTED") { + } else if (jobStatus === "RUNNING") { outputs = [ Running... , ]; - } else if ( - jobStatus === "FAILED" || - jobStatus === "ABORTED" || - (jobStatus.startsWith("JOB_") && jobStatus.endsWith("_FAILURE")) - ) { - outputs = "Failed - no outputs"; - } else if ( - jobStatus === "PENDING" || - jobStatus === "WAITING_FOR_CLUSTER" || - jobStatus === "PUBLISHED" || - jobStatus === "NOT_STARTED" || - jobStatus === "WAITING_FOR_RESOURCES" || - (jobStatus.startsWith("JOB_") && !jobStatus.endsWith("_FAILURE")) - ) { - outputs = "Pending to run"; - } else if (jobStatus === "UNKNOWN") { - outputs = "Please try again later"; - } else { - outputs = "Unknown status, please contact support"; - } + } else if (jobStatus === "FAILED") outputs = "Failed - no outputs"; + else if (jobStatus === "PENDING") outputs = "Pending to run"; + else if (jobStatus === "UNKNOWN") outputs = "Unknown status, please try again later"; + else outputs = "Unknown status, please contact support"; const inputs = Object.entries(job.job.inputs).map(([key, value]) => ( @@ -192,7 +176,7 @@ function JobRow(props: JobRowProps) { size="small" disabled={ creatingJobCopy || - (!jobStatus.includes("SUCCESS") && !(jobStatus.includes("FAILED") || jobStatus.includes("FAILURE"))) + (!jobStatus.includes("SUCCESS") && !(jobStatus.includes("FAILED"))) } onClick={async () => { setCreatingJobCopy(true); diff --git a/node/src/components/data/JobSelector.tsx b/node/src/components/data/JobSelector.tsx index a645a694..bffe4956 100644 --- a/node/src/components/data/JobSelector.tsx +++ b/node/src/components/data/JobSelector.tsx @@ -28,6 +28,7 @@ import { getFunctionJobsFromFunctionJobCollection, getJobCollectionStatus, filterForFinalStatus, + extractJobStatus, } from "../../utils/function_utils"; import getMinMax from "../minmax"; import CustomTooltip from "../utils/CustomTooltip"; @@ -74,7 +75,7 @@ export default function JobsSelector(props: JobSelectorPropsType) { const auxJob = jc; if (jc.jobCollection.uid === uid) { auxJob.subJobs = auxJob.subJobs.map(j => ({ - selected: selected === true ? j.job.status === "SUCCESS" : false, + selected: selected === true ? extractJobStatus(j) === "SUCCESS" : false, job: j.job, })); auxJob.selected = selected === true ? auxJob.subJobs.some(j => j.selected === true) : false; @@ -149,11 +150,7 @@ export default function JobsSelector(props: JobSelectorPropsType) { return ( fetchedJC !== undefined && jc.jobIds.join(",") === fetchedJC.subJobs.map(j => j.job.uid).join(",") && - fetchedJC.subJobs.every(j => - typeof j.job.status === "string" - ? filterForFinalStatus(j.job.status) - : filterForFinalStatus((j.job.status as unknown as { status: string }).status), - ) + fetchedJC.subJobs.every(j => extractJobStatus(j)) ); }); @@ -198,25 +195,20 @@ export default function JobsSelector(props: JobSelectorPropsType) { j.subJobs.some( sj => sj.job.uid === id && - filterForFinalStatus( - typeof sj.job.status === "string" - ? sj.job.status - : (sj.job.status as unknown as { status: string }).status, + filterForFinalStatus(extractJobStatus(sj) ), ), ); if (existingJob) { job = existingJob.subJobs.find(j => j.job.uid === id)?.job; - job.status = typeof job.status === "string" ? job.status : (job.status as unknown as { status: string }).status; } else { job = functionJobs[subJobIdx]; - job.status = typeof job.status === "string" ? job.status : (job.status as unknown as { status: string }).status; } jobsFetched.current += 1; const jobsProg = (jobsFetched.current / totalSubs) * 100; setJobProgress(jobsProg); subJobs.push({ - selected: job.status === "SUCCESS", + selected: extractJobStatus(job) === "SUCCESS", job, }); } @@ -286,7 +278,7 @@ export default function JobsSelector(props: JobSelectorPropsType) { const newJobCollections: SelectedJobCollection[] = jobCollections.map(jc => { const auxJob = jc; auxJob.subJobs = jc.subJobs.map(subJob => ({ - selected: checked === true ? subJob.job.status === "SUCCESS" : false, + selected: checked === true ? extractJobStatus(subJob) === "SUCCESS" : false, job: subJob.job, })); const auxJobState = auxJob.subJobs.map(j => j.selected); @@ -300,22 +292,6 @@ export default function JobsSelector(props: JobSelectorPropsType) { [jobCollections, updateJobContext], ); - // const autoSelectJobs = useCallback(() => { - // const newJobCollections: SelectedJobCollection[] = jobCollections.map(jc => { - // const auxJob = jc; - // auxJob.subJobs = jc.subJobs.map(subJob => ({ - // selected: subJob.job.status === "SUCCESS", - // job: subJob.job, - // })); - // const auxJobState = auxJob.subJobs.map(j => j.selected); - // auxJob.selected = !auxJobState.every(j => j === false); - // return auxJob; - // }); - - // setJobCollections(newJobCollections); - // updateJobContext(newJobCollections); - // }, [jobCollections, updateJobContext]); - const handleJobsUpdate = useCallback(async () => { await updateJobCollections(selectedFunction?.uid as string); console.info("Updated JobCollections"); @@ -412,7 +388,7 @@ export default function JobsSelector(props: JobSelectorPropsType) { indeterminate={ jobCollections.some(jc => jc.selected === true) && !jobCollections.every( - jc => jc.subJobs.map(j => j.job).filter(j => j.status === "SUCCESS" && j.selected === true).length > 0, + jc => jc.subJobs.map(j => j.job).filter(j => extractJobStatus(j) === "SUCCESS" && j.selected === true).length > 0, ) } onChange={event => onToggleAll(event.target.checked)} @@ -425,7 +401,7 @@ export default function JobsSelector(props: JobSelectorPropsType) { checked={params.row.selected} indeterminate={params.row.subJobs.some(j => j.selected) && !params.row.subJobs.every(j => j.selected)} onChange={event => selectMainJob(params.row.jobCollection.uid, event.target.checked)} - disabled={params.row.subJobs.every((j: SubJob) => j.job.status !== "SUCCESS")} + disabled={params.row.subJobs.every((j: SubJob) => extractJobStatus(j) !== "SUCCESS")} inputProps={{ "aria-label": "Select job collection" }} sx={theme => ({ "& .MuiSvgIcon-root": { color: `${theme.palette.primary.main} !important` } })} /> diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index 65dd7689..126669a4 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -167,17 +167,77 @@ export type JobStatusCounts = { unknown: number; }; +export type AllowedJobStatus = "SUCCESS" | "FAILED" | "RUNNING" | "PENDING" | "UNKNOWN"; + + +export function extractJobStatus(job: FunctionJob | SubJob): AllowedJobStatus { + function classifyJobStatus(jobStatus: string): AllowedJobStatus { + // This function helps homogenize job status, centralizing all corresponding logic + const status = jobStatus + if (!jobStatus) { + throw new Error("JobStatus is undefined!") + } + + if (jobStatus === "SUCCESS") { + return "SUCCESS"; + } + else if (status.endsWith("FAILED") || status.endsWith("FAILURE")) { + return "FAILED" + } + else if (status === "STARTED" || status === "RUNNING") { + return "RUNNING" + } + else if (status === "PENDING" || status.startsWith("JOB_") || status === "WAITING_") { + return "PENDING" + } + else { + console.warn("Could not classify JobStatus", jobStatus) + return "UNKNOWN" + } + + } + + if (!job) { + throw new Error("Job is undefined"); + } + + // Check if job is of type SubJob (has 'selected' and 'job' properties) + if (typeof job === "object" && "selected" in job && "job" in job) { + // job is a SubJob, so use recursivity to extract status from its 'job' property + return extractJobStatus(job.job); + // previous way: + // typeof sj.job.status === "string" + // ? sj.job.status + // : (sj.job.status as unknown as { status: string }).status, + } + + if (typeof job.status === "string") { + return classifyJobStatus(job.status); + } + else if (job.status && typeof job.status === "object" && "status" in job.status) { + return classifyJobStatus((job.status as { status: string }).status); + } + else { + console.log("Could not extract status of job ", job) + return "UNKNOWN"; + } + } + export function getJobStatusCounts(subJobs: SubJob[]): JobStatusCounts { return subJobs .filter(j => j.job) - .map(j => (typeof j.job.status === "string" ? j.job.status : (j.job.status as unknown as { status: string }).status)) + .map(j => extractJobStatus(j.job)) .reduce( - (acc, status: string) => { + (acc, status: AllowedJobStatus) => { if (status === "SUCCESS") acc.success += 1; - else if (status.endsWith("FAILED") || status.endsWith("FAILURE")) acc.failed += 1; - else if (status === "STARTED" || status === "RUNNING") acc.running += 1; - else if (status === "PENDING" || status.startsWith("JOB_") || status === "WAITING_") acc.pending += 1; - else acc.unknown += 1; + else if (status === "FAILED") acc.failed += 1; + else if (status === "RUNNING") acc.running += 1; + else if (status === "PENDING") acc.pending += 1; + else if (status === "UNKNOWN") acc.unknown += 1; + else { + console.warn("status should have been classified into one of the AllowedJobStatus!") + console.warn("status: ", status) + }; return acc; }, { success: 0, failed: 0, running: 0, pending: 0, unknown: 0 }, @@ -187,10 +247,6 @@ export function getJobStatusCounts(subJobs: SubJob[]): JobStatusCounts { export function getJobCollectionStatus(subJobs: SubJob[]) { if (!subJobs || subJobs.length === 0) return "NO JOBS"; const jobStatusCounts = getJobStatusCounts(subJobs); - if (jobStatusCounts.unknown > 0) { - // toast.warn("Could not classify some job statuses - please revise console logs") - console.warn("SubJobs that gave UNKNOWN status: ", subJobs); - } const allSuccess = jobStatusCounts.success === subJobs.length; const anySuccess = jobStatusCounts.success > 0; const anyRunning = jobStatusCounts.running > 0; @@ -202,9 +258,9 @@ export function getJobCollectionStatus(subJobs: SubJob[]) { if (anyRunning) return "RUNNING"; if (anyPending) return "PENDING"; if (anyFailed && anySuccess) return "FAILED PARTIALLY"; - return "UNKNOWN"; + else return "UNKNOWN"; } export function filterForFinalStatus(status: string) { - return status === "FAILED" || status === "SUCCESS" || status.includes("FAILURE"); + return status === "FAILED" || status === "SUCCESS"; } From 9a3be2289f3878a3d070a97d7386a66666c829ab Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Tue, 7 Oct 2025 21:54:43 +0200 Subject: [PATCH 02/11] fix status-as-dict handling in the flaskapi --- flaskapi/flask_workflows.py | 25 ++++++++++++++----- node/src/utils/function_utils.ts | 42 +++++++++++++++----------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/flaskapi/flask_workflows.py b/flaskapi/flask_workflows.py index c00b9af9..5fc164ea 100644 --- a/flaskapi/flask_workflows.py +++ b/flaskapi/flask_workflows.py @@ -480,9 +480,17 @@ def _timeit(fun: Callable, *args, **kwargs): # test_job_retrieval_paginated(function_uid="eea21c0d-6c2b-4cf4-91d1-116e6550cb22") +def _get_job_status(job: Dict[str, Any]) -> str: + status = job["status"] + if isinstance(status, dict) and "status" in status: + return status["status"] + elif isinstance(status, str): + return status + else: + raise ValueError(f"Unknown status format: {status}") def _check_jobs(jobs: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - completed_jobs = [job for job in jobs if job["status"].lower() == "completed" or job["status"].lower() == "success"] # type: ignore + completed_jobs = [job for job in jobs if _get_job_status(job).lower() == "completed" or _get_job_status(job).lower() == "success"] # type: ignore for job in completed_jobs: assert "outputs" in job, f"No outputs key found for completed job: {job} with status: {job['status']}" # type: ignore @@ -965,11 +973,16 @@ def flask_test_job(): ), f"Job is None for function {function_uid} with sample {sample}. Response: {response}" uid = response.actual_instance.uid _logger.debug(f"Job UID: {uid}") - while ( - "JOB_TASK_" in (job := _get_function_job_from_uid(uid))["status"] - and not "FAILURE" in job - ): - time.sleep(1) + while True: + job = _get_function_job_from_uid(uid) + job_status = _get_job_status(job) + _logger.debug(f"Job status: {job_status}") + if "FAILURE" in job_status: + raise RuntimeError(f"Job {uid} failed with status: {job_status}") + elif not "JOB_TASK_" in job_status: + break ## exit the loop if the job has been initialized + else: + time.sleep(1) _logger.debug(f"Created job: {job}") return jsonify(job) # return the job details as a dictionary except Exception as e: diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index 126669a4..029ac6f4 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -173,28 +173,26 @@ export type AllowedJobStatus = "SUCCESS" | "FAILED" | "RUNNING" | "PENDING" | "U export function extractJobStatus(job: FunctionJob | SubJob): AllowedJobStatus { function classifyJobStatus(jobStatus: string): AllowedJobStatus { // This function helps homogenize job status, centralizing all corresponding logic - const status = jobStatus - if (!jobStatus) { - throw new Error("JobStatus is undefined!") - } - - if (jobStatus === "SUCCESS") { - return "SUCCESS"; - } - else if (status.endsWith("FAILED") || status.endsWith("FAILURE")) { - return "FAILED" - } - else if (status === "STARTED" || status === "RUNNING") { - return "RUNNING" - } - else if (status === "PENDING" || status.startsWith("JOB_") || status === "WAITING_") { - return "PENDING" - } - else { - console.warn("Could not classify JobStatus", jobStatus) - return "UNKNOWN" - } - + if (!jobStatus) { + throw new Error("JobStatus is undefined!") + } + + if (jobStatus === "SUCCESS") { + return "SUCCESS"; + } + else if (jobStatus.endsWith("FAILED") || jobStatus.endsWith("FAILURE")) { + return "FAILED" + } + else if (jobStatus === "STARTED" || jobStatus === "RUNNING") { + return "RUNNING" + } + else if (jobStatus === "PENDING" || jobStatus.startsWith("JOB_") || jobStatus === "WAITING_") { + return "PENDING" + } + else { + console.warn("Could not classify JobStatus", jobStatus) + return "UNKNOWN" + } } if (!job) { From 6a3a5f907337abe9166e282d1b3f2240e43b9f8a Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Tue, 14 Oct 2025 12:27:31 +0200 Subject: [PATCH 03/11] additional changes & refactoring --- node/src/components/data/JobSelector.tsx | 2 +- node/src/context/JobContext.tsx | 53 ++++------ node/src/utils/function_utils.ts | 122 ++++++++++++++++------- 3 files changed, 107 insertions(+), 70 deletions(-) diff --git a/node/src/components/data/JobSelector.tsx b/node/src/components/data/JobSelector.tsx index ed6beb2a..a9b8c29b 100644 --- a/node/src/components/data/JobSelector.tsx +++ b/node/src/components/data/JobSelector.tsx @@ -27,7 +27,7 @@ import { getFunctionJobCollections, getFunctionJobsFromFunctionJobCollection, getJobCollectionStatus, - filterForFinalStatus, + isFinalStatus, extractJobStatus, } from "../../utils/function_utils"; import getMinMax from "../minmax"; diff --git a/node/src/context/JobContext.tsx b/node/src/context/JobContext.tsx index cd4bd5d0..c7f49053 100644 --- a/node/src/context/JobContext.tsx +++ b/node/src/context/JobContext.tsx @@ -7,7 +7,10 @@ import { PersistenceType } from "./types"; import { getFunctionJobCollections, getFunctionJobsFromFunctionJobCollection, - filterForFinalStatus, + isFinalStatus, + extractJobStatus, + extractJobOutputs, + AllowedJobStatus, } from "../utils/function_utils"; export interface JobContextType { @@ -20,7 +23,7 @@ export interface JobContextType { allJobsList: () => FunctionJob[]; filteredJobList: FunctionJob[]; requestForceFetch: (functionUID: string, progress: (progress: number) => void) => void; - parseStatus: (jobStatus: string, outputArray: Record) => string | JSX.Element[]; + getOutputsForTable: (job: FunctionJob | SubJob) => string | JSX.Element[]; } export const JobContext = createContext(undefined); @@ -37,23 +40,12 @@ export function JobContextProvider({ children }: Props) { const [fetchedJobCollections, setFetchedJobCollections] = useState(undefined); const [runningJobCollection, setRunningJobCollection] = useState(undefined); - // Filter out job status that are not strings - const jobStatusFilter = (status: unknown) => { - if (typeof status === "string") { - return status; - } - if (typeof status === "object" && status !== null) { - if ("status" in status && typeof status.status === "string") { - return status.status; - } - } - console.log("job status is UNKNOWN", status); - return "UNKNOWN"; - }; - - const parseStatus = (jobStatusUnk: unknown, outputArray: Record): string | JSX.Element[] => { - const jobStatus = jobStatusFilter(jobStatusUnk); - let outputs; + // TODO change all calls to this function!! bfr Alex was passing status + outputs -- here, just pass job + const getOutputsForTable = (job: FunctionJob | SubJob): string | JSX.Element[] => { + const jobStatus: AllowedJobStatus = extractJobStatus(job); + const outputArray: Record = extractJobOutputs(job); + + let outputs: string | JSX.Element[]; if (jobStatus === "SUCCESS") { outputs = Object.entries(outputArray).map(([key, value]) => ( @@ -61,21 +53,18 @@ export function JobContextProvider({ children }: Props) { {", "} )); - } else if (jobStatus === "STARTED") { + } else if (jobStatus === "RUNNING") { outputs = [ Running... , ]; - } else if (["FAILED", "ABORTED"].includes(jobStatus) || (jobStatus.startsWith("JOB_") && jobStatus.endsWith("_FAILURE"))) { + } else if (jobStatus === "FAILED") { outputs = "Failed - no outputs"; - } else if ( - ["PENDING", "WAITING_FOR_CLUSTER", "PUBLISHED", "NOT_STARTED", "WAITING_FOR_RESOURCES"].includes(jobStatus) || - (jobStatus.startsWith("JOB_") && !jobStatus.endsWith("_FAILURE")) - ) { + } else if (jobStatus === "PENDING") { outputs = "Pending to run"; } else if (jobStatus === "UNKNOWN") { - outputs = "Please try again later"; + outputs = "Unknown status, please try again later"; } else { outputs = "Unknown status, please contact support"; } @@ -98,12 +87,12 @@ export function JobContextProvider({ children }: Props) { const fetchedJCMap = new Map(fetchedJobCollections && fetchedJobCollections.map(fjc => [fjc.jobCollection.uid, fjc])); const equalJC: boolean[] = jobsC.map(jc => { const fetchedJC = fetchedJCMap.get(jc.uid); - const statusList = fetchedJC ? fetchedJC.subJobs.map(j => jobStatusFilter(j.job.status)) : []; + const statusList = fetchedJC ? fetchedJC.subJobs.map(j => extractJobStatus(j)) : []; return ( fetchedJC !== undefined && fetchedJC.subJobs.map(j => j.job.uid).every(jcUID => jc.jobIds.includes(jcUID)) && fetchedJC.subJobs.length === jc.jobIds.length && - statusList.every(s => filterForFinalStatus(s)) + statusList.every(s => isFinalStatus(s)) ); }); @@ -127,13 +116,13 @@ export function JobContextProvider({ children }: Props) { const subJobs = []; for (let subJobIdx = 0; subJobIdx < functionJobs.length; subJobIdx += 1) { const job: FunctionJob = functionJobs[subJobIdx]; - job.status = jobStatusFilter(job.status); + const jobStatus = extractJobStatus(job); jobsFetched += 1; const jobsProg = (jobsFetched / totalSubs) * 100; progress(jobsProg); const existingSelected = oldSubJobs.find(sj => sj.job.uid === job.uid)?.selected; subJobs.push({ - selected: existingSelected !== undefined ? existingSelected : job.status === "SUCCESS", + selected: existingSelected !== undefined ? existingSelected : jobStatus === "SUCCESS", job, }); } @@ -227,7 +216,7 @@ export function JobContextProvider({ children }: Props) { allJobsList, filteredJobList, requestForceFetch, - parseStatus, + getOutputsForTable, }), [ runningJobCollection, @@ -239,7 +228,7 @@ export function JobContextProvider({ children }: Props) { allJobsList, filteredJobList, requestForceFetch, - parseStatus, + getOutputsForTable, ], ); return {children}; diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index a862f3f0..529e56af 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -169,57 +169,105 @@ export type JobStatusCounts = { export type AllowedJobStatus = "SUCCESS" | "FAILED" | "RUNNING" | "PENDING" | "UNKNOWN"; +export function isSubJob(job: FunctionJob | SubJob): job is SubJob { + if (!job) { + throw new Error("Job is undefined"); + } -export function extractJobStatus(job: FunctionJob | SubJob): AllowedJobStatus { - function classifyJobStatus(jobStatus: string): AllowedJobStatus { - // This function helps homogenize job status, centralizing all corresponding logic - if (!jobStatus) { - throw new Error("JobStatus is undefined!") - } - - if (jobStatus === "SUCCESS") { - return "SUCCESS"; - } - else if (jobStatus.endsWith("FAILED") || jobStatus.endsWith("FAILURE")) { - return "FAILED" - } - else if (jobStatus === "STARTED" || jobStatus === "RUNNING") { - return "RUNNING" - } - else if (jobStatus === "PENDING" || jobStatus.startsWith("JOB_") || jobStatus.startsWith("WAITING_") || jobStatus === "PUBLISHED") { - return "PENDING" - } - else { - console.warn("Could not classify JobStatus", jobStatus) - return "UNKNOWN" - } - } + if (typeof job === "object") { + return (job as SubJob).selected !== undefined && (job as SubJob).job !== undefined; + } + else { + return false; + } +} - if (!job) { +export function isFunctionJob(job: FunctionJob | SubJob): job is FunctionJob { + if (!job) { throw new Error("Job is undefined"); - } + } + + if (typeof job === "object") { + return (job as FunctionJob).inputs !== undefined && (job as FunctionJob).functionUid !== undefined && (job as FunctionJob).status !== undefined; + } + else { + return false; + } +} - // Check if job is of type SubJob (has 'selected' and 'job' properties) - if (typeof job === "object" && "selected" in job && "job" in job) { - // job is a SubJob, so use recursivity to extract status from its 'job' property - return extractJobStatus(job.job); - // previous way: - // typeof sj.job.status === "string" - // ? sj.job.status - // : (sj.job.status as unknown as { status: string }).status, +function classifyJobStatus(jobStatus: string): AllowedJobStatus { + // This function helps homogenize job status into four categories + unknown, + // centralizing all corresponding logic + if (!jobStatus) { + throw new Error("JobStatus is undefined!") + } + + if (jobStatus === "SUCCESS") { + return "SUCCESS"; } + else if (jobStatus.endsWith("FAILED") || jobStatus.endsWith("FAILURE")) { + return "FAILED" + } + else if (jobStatus === "STARTED" || jobStatus === "RUNNING") { + return "RUNNING" + } + else if (jobStatus === "PENDING" || jobStatus.startsWith("JOB_") || jobStatus.startsWith("WAITING_") || jobStatus === "PUBLISHED") { + return "PENDING" + } + else { + console.warn("Could not classify JobStatus", jobStatus) + return "UNKNOWN" + } +} +export function extractJobStatus(job: FunctionJob | SubJob): AllowedJobStatus { + // This function extracts the job status from either a FunctionJob or a SubJob + // allowing for status to be either a string or an object with a status field + // and classifies it into one of the AllowedJobStatus categories + if (isFunctionJob(job)) { if (typeof job.status === "string") { return classifyJobStatus(job.status); } - else if (job.status && typeof job.status === "object" && "status" in job.status) { + else if (job.status && typeof job.status === "object" && "status" in job.status && typeof job.status.status === "string") { return classifyJobStatus((job.status as { status: string }).status); } else { - console.log("Could not extract status of job ", job) + console.log(`job status ${job.status} could not be extracted, classifying as UNKNOWN.`); return "UNKNOWN"; } } + // If it's a SubJob, recurse to extract from the inner job + else if (isSubJob(job)) { + return extractJobStatus(job.job); + } + else { + throw new Error("Job passed to extractJobStatus is neither FunctionJob nor SubJob!"); + } + } + +export function extractJobOutputs(job: FunctionJob | SubJob): Record { + // This function extracts the job outputs from either a FunctionJob or a SubJob + // allowing for outputs to be either a Record or an object with an outputs field + if (isFunctionJob(job)) { + if (job.outputs && typeof job.outputs === "object") { + return job.outputs as Record; + } + else if (job.outputs && typeof job.outputs === "object" && "outputs" in job.outputs && typeof job.outputs.outputs === "object") { + return job.outputs.outputs as Record; + } + else { + console.log(`job outputs ${job.outputs} could not be extracted, returning empty object.`); + return {}; + } + } + // If it's a SubJob, recurse to extract from the inner job + else if (isSubJob(job)) { + return extractJobOutputs(job.job); + } + else { + throw new Error("Job passed to extractJobOutputs is neither FunctionJob nor SubJob!"); + } +} export function getJobStatusCounts(subJobs: SubJob[]): JobStatusCounts { return subJobs @@ -259,6 +307,6 @@ export function getJobCollectionStatus(subJobs: SubJob[]) { else return "UNKNOWN"; } -export function filterForFinalStatus(status: string) { +export function isFinalStatus(status: string) { return status === "FAILED" || status === "SUCCESS"; } From 1271bb2853cedd8f8eb34a6aa5eaa7b8827e615d Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Tue, 14 Oct 2025 16:37:46 +0200 Subject: [PATCH 04/11] fix altered function signature in test --- node/src/utils/utils.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/src/utils/utils.test.ts b/node/src/utils/utils.test.ts index b5669787..467bc92c 100644 --- a/node/src/utils/utils.test.ts +++ b/node/src/utils/utils.test.ts @@ -11,6 +11,7 @@ import { getSamplingEndValue, getSamplingStartValue } from "./sampling"; import { stepValidator } from "./stepValidator"; import { RegisteredFunctionJobCollection, FunctionJob } from "../osparc-api-ts-client"; import type { Function as OsparcFunction } from "../osparc-api-ts-client"; +import { getOutputsForTable } from "./table_utils"; // 1st test: get the file with a given path describe("CSV Functions", () => { @@ -195,7 +196,7 @@ describe("stepValidator", () => { requestForceFetch: (): void => { throw new Error("Function not implemented."); }, - parseStatus: (_jobStatus: string, _outputArray: Record): string | JSX.Element[] => { + getOutputsForTable: (_job: FunctionJob | SubJob): string | JSX.Element[] => { throw new Error("Function not implemented."); }, }; From 3852525ac9e152113950fbf9418825179eb085df Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Tue, 14 Oct 2025 16:43:16 +0200 Subject: [PATCH 05/11] remove spurious AI import --- node/src/utils/utils.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/node/src/utils/utils.test.ts b/node/src/utils/utils.test.ts index 467bc92c..3008a68e 100644 --- a/node/src/utils/utils.test.ts +++ b/node/src/utils/utils.test.ts @@ -11,7 +11,6 @@ import { getSamplingEndValue, getSamplingStartValue } from "./sampling"; import { stepValidator } from "./stepValidator"; import { RegisteredFunctionJobCollection, FunctionJob } from "../osparc-api-ts-client"; import type { Function as OsparcFunction } from "../osparc-api-ts-client"; -import { getOutputsForTable } from "./table_utils"; // 1st test: get the file with a given path describe("CSV Functions", () => { From 2a64f904b3f2a0cf213db13363eac178e960c9b1 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez <56032114+JavierGOrdonnez@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:53:03 +0200 Subject: [PATCH 06/11] Update node/src/utils/function_utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- node/src/utils/function_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index 529e56af..e21e36e9 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -206,7 +206,7 @@ function classifyJobStatus(jobStatus: string): AllowedJobStatus { return "SUCCESS"; } else if (jobStatus.endsWith("FAILED") || jobStatus.endsWith("FAILURE")) { - return "FAILED" + return "FAILED"; } else if (jobStatus === "STARTED" || jobStatus === "RUNNING") { return "RUNNING" From ef06a7bd47d579d79d9cddf39aad4afcbd4c3de1 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez <56032114+JavierGOrdonnez@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:53:12 +0200 Subject: [PATCH 07/11] Update node/src/utils/function_utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- node/src/utils/function_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index e21e36e9..de6e13ee 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -209,7 +209,7 @@ function classifyJobStatus(jobStatus: string): AllowedJobStatus { return "FAILED"; } else if (jobStatus === "STARTED" || jobStatus === "RUNNING") { - return "RUNNING" + return "RUNNING"; } else if (jobStatus === "PENDING" || jobStatus.startsWith("JOB_") || jobStatus.startsWith("WAITING_") || jobStatus === "PUBLISHED") { return "PENDING" From 1bb7203e435ba350343fbea88658fe15501bd7c5 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez <56032114+JavierGOrdonnez@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:53:21 +0200 Subject: [PATCH 08/11] Update node/src/utils/function_utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- node/src/utils/function_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index de6e13ee..c028d14f 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -212,7 +212,7 @@ function classifyJobStatus(jobStatus: string): AllowedJobStatus { return "RUNNING"; } else if (jobStatus === "PENDING" || jobStatus.startsWith("JOB_") || jobStatus.startsWith("WAITING_") || jobStatus === "PUBLISHED") { - return "PENDING" + return "PENDING"; } else { console.warn("Could not classify JobStatus", jobStatus) From dffbd227182a2e19baad790e81d073ebb58a6d2d Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez <56032114+JavierGOrdonnez@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:53:31 +0200 Subject: [PATCH 09/11] Update node/src/utils/function_utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- node/src/utils/function_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index c028d14f..26c7bbea 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -216,7 +216,7 @@ function classifyJobStatus(jobStatus: string): AllowedJobStatus { } else { console.warn("Could not classify JobStatus", jobStatus) - return "UNKNOWN" + return "UNKNOWN"; } } From 00ad4bc367a5fd5b7fd4806b43713c5299886f4f Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez <56032114+JavierGOrdonnez@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:53:40 +0200 Subject: [PATCH 10/11] Update node/src/utils/function_utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- node/src/utils/function_utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/utils/function_utils.ts b/node/src/utils/function_utils.ts index 26c7bbea..e2bdd0c2 100644 --- a/node/src/utils/function_utils.ts +++ b/node/src/utils/function_utils.ts @@ -281,8 +281,8 @@ export function getJobStatusCounts(subJobs: SubJob[]): JobStatusCounts { else if (status === "PENDING") acc.pending += 1; else if (status === "UNKNOWN") acc.unknown += 1; else { - console.warn("status should have been classified into one of the AllowedJobStatus!") - console.warn("status: ", status) + console.warn("status should have been classified into one of the AllowedJobStatus!"); + console.warn("status: ", status); }; return acc; }, From fbb2fb75ba10190f0fcad5bdd3430f65b35e5244 Mon Sep 17 00:00:00 2001 From: Javier Garcia Ordonez Date: Tue, 14 Oct 2025 16:54:27 +0200 Subject: [PATCH 11/11] remove resolved TODO --- node/src/context/JobContext.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/node/src/context/JobContext.tsx b/node/src/context/JobContext.tsx index c7f49053..a68db15c 100644 --- a/node/src/context/JobContext.tsx +++ b/node/src/context/JobContext.tsx @@ -40,7 +40,6 @@ export function JobContextProvider({ children }: Props) { const [fetchedJobCollections, setFetchedJobCollections] = useState(undefined); const [runningJobCollection, setRunningJobCollection] = useState(undefined); - // TODO change all calls to this function!! bfr Alex was passing status + outputs -- here, just pass job const getOutputsForTable = (job: FunctionJob | SubJob): string | JSX.Element[] => { const jobStatus: AllowedJobStatus = extractJobStatus(job); const outputArray: Record = extractJobOutputs(job);