diff --git a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json index d29a18c691..e7ed0aee65 100644 --- a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json +++ b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json @@ -46,11 +46,11 @@ ] }, "nullable": [ - true, - true, - true, - true, - true, + false, + false, + false, + false, + false, true, true ] diff --git a/backend/.sqlx/query-73d1019fdff3113fb0c561048273ba57bf2364adebb895eba57896895b96f8d4.json b/backend/.sqlx/query-73d1019fdff3113fb0c561048273ba57bf2364adebb895eba57896895b96f8d4.json new file mode 100644 index 0000000000..d6bd855405 --- /dev/null +++ b/backend/.sqlx/query-73d1019fdff3113fb0c561048273ba57bf2364adebb895eba57896895b96f8d4.json @@ -0,0 +1,42 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n j.created_at AS \"created_at!\",\n c.started_at,\n c.duration_ms,\n j.created_by AS \"created_by!\"\n FROM v2_job_completed c\n JOIN v2_job j USING (id)\n WHERE c.id = $1 AND c.workspace_id = $2 AND ($3::text[] IS NULL OR j.tag = ANY($3))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at!", + "type_info": "Timestamptz" + }, + { + "ordinal": 1, + "name": "started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "duration_ms", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "created_by!", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "TextArray" + ] + }, + "nullable": [ + false, + true, + false, + false + ] + }, + "hash": "73d1019fdff3113fb0c561048273ba57bf2364adebb895eba57896895b96f8d4" +} diff --git a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json index 99269c9851..54e94cfb8f 100644 --- a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json +++ b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json @@ -18,8 +18,8 @@ "Left": [] }, "nullable": [ - true, - false + false, + true ] }, "hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76" diff --git a/backend/.sqlx/query-b4eb72b0274cbdce7490f63c36d0d16ee847294fadc138593a1baa417cbb3652.json b/backend/.sqlx/query-b4eb72b0274cbdce7490f63c36d0d16ee847294fadc138593a1baa417cbb3652.json index d16886016b..f1ba61b895 100644 --- a/backend/.sqlx/query-b4eb72b0274cbdce7490f63c36d0d16ee847294fadc138593a1baa417cbb3652.json +++ b/backend/.sqlx/query-b4eb72b0274cbdce7490f63c36d0d16ee847294fadc138593a1baa417cbb3652.json @@ -59,9 +59,7 @@ "failure", "command", "approval", - "preprocessor", - "schedule_handler_old", - "dynamic_skip" + "preprocessor" ] } } diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 4b0dab9d26..3bdcef1b0f 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -9717,6 +9717,34 @@ paths: - completed - result + /w/{workspace}/jobs_u/completed/get_timing/{id}: + get: + summary: get completed job timing + operationId: getCompletedJobTiming + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + responses: + "200": + description: job timing details + content: + application/json: + schema: + type: object + properties: + created_at: + type: string + format: date-time + started_at: + type: string + format: date-time + duration_ms: + type: integer + required: + - created_at + /w/{workspace}/jobs/completed/delete/{id}: post: summary: delete completed job (erase content but keep run id) diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index d899047942..e6160d5f0b 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -301,6 +301,10 @@ pub fn workspaced_service() -> Router { "/completed/get_result_maybe/:id", get(get_completed_job_result_maybe).layer(cors.clone()), ) + .route( + "/completed/get_timing/:id", + get(get_completed_job_timing).layer(cors.clone()), + ) .route( "/completed/delete/:id", post(delete_completed_job).layer(cors.clone()), @@ -377,6 +381,7 @@ pub fn workspace_unauthed_service() -> Router { "/completed/get_result_maybe/:id", get(get_completed_job_result_maybe), ) + .route("/completed/get_timing/:id", get(get_completed_job_timing)) .route("/getupdate/:id", get(get_job_update)) .route("/getupdate_sse/:id", get(get_job_update_sse)) .route("/get_log_file/*file_path", get(get_log_file)) @@ -8561,6 +8566,54 @@ async fn get_completed_job_result_maybe( } } +#[derive(Serialize)] +struct JobTiming { + created_at: chrono::DateTime, + started_at: Option>, + duration_ms: Option, +} + +async fn get_completed_job_timing( + OptAuthed(opt_authed): OptAuthed, + Extension(db): Extension, + Path((w_id, id)): Path<(String, Uuid)>, +) -> error::JsonResult { + let tags = opt_authed + .as_ref() + .map(|authed| get_scope_tags(authed)) + .flatten(); + + let result = sqlx::query!( + "SELECT + j.created_at AS \"created_at!\", + c.started_at, + c.duration_ms, + j.created_by AS \"created_by!\" + FROM v2_job_completed c + JOIN v2_job j USING (id) + WHERE c.id = $1 AND c.workspace_id = $2 AND ($3::text[] IS NULL OR j.tag = ANY($3))", + id, + &w_id, + tags.as_ref().map(|v| v.as_slice()) as Option<&[&str]>, + ) + .fetch_optional(&db) + .await?; + + let result = not_found_if_none(result, "Completed Job", id.to_string())?; + + if opt_authed.is_none() && result.created_by != "anonymous" { + return Err(Error::BadRequest( + "As a non logged in user, you can only see jobs ran by anonymous users".to_string(), + )); + } + + Ok(Json(JobTiming { + created_at: result.created_at, + started_at: result.started_at, + duration_ms: Some(result.duration_ms), + })) +} + async fn delete_completed_job<'a>( authed: ApiAuthed, Tokened { token }: Tokened, diff --git a/frontend/src/lib/components/flows/CreateActionsApp.svelte b/frontend/src/lib/components/flows/CreateActionsApp.svelte index 5352a99d01..771698c93c 100644 --- a/frontend/src/lib/components/flows/CreateActionsApp.svelte +++ b/frontend/src/lib/components/flows/CreateActionsApp.svelte @@ -11,12 +11,11 @@ import Badge from '$lib/components/common/badge/Badge.svelte' import YAML from 'yaml' - import Tooltip from '../Tooltip.svelte' let drawer: Drawer | undefined = undefined - let pendingRaw: string = '' + let pendingRaw: string = $state('') - let importType: 'yaml' | 'json' = 'yaml' + let importType: 'yaml' | 'json' = $state('yaml') // Modal states let appTypeModalOpen = $state(false) diff --git a/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte index 237c6f3bf2..7de7cab759 100644 --- a/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte @@ -46,6 +46,21 @@ if (event.data.type == 'backend') { respond({ result, error }) } + if (editor) { + try { + let jobInfo = await JobService.getCompletedJobTiming({ workspace, id: uuid }) + if (jobInfo.started_at) { + jobsById[uuid] = { + ...(jobsById[uuid] ?? {}), + created_at: new Date(jobInfo.created_at).getTime(), + started_at: new Date(jobInfo.started_at).getTime(), + duration_ms: jobInfo.duration_ms + } + } + } catch (e) { + console.error('Error getting job info', e) + } + } return result } if (event.data.type == 'backend' || event.data.type == 'backendAsync') { @@ -100,7 +115,7 @@ } else if (event.data.type == 'waitJob') { await respondWithResult(data.jobId) } else if (event.data.type == 'getJob') { - const job = JobService.getJob({ workspace, id: data.jobId }) + const job = await JobService.getJob({ workspace, id: data.jobId }) respond({ result: job }) } else { console.error('No runnable found for', runnable_id)