add proper jobs timings for raw apps

This commit is contained in:
Ruben Fiszel
2025-12-23 11:39:48 +00:00
parent ad2232e4cb
commit 91a00857a8
8 changed files with 149 additions and 14 deletions
@@ -46,11 +46,11 @@
]
},
"nullable": [
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
true,
true
]
@@ -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"
}
@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
true,
false
false,
true
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
@@ -59,9 +59,7 @@
"failure",
"command",
"approval",
"preprocessor",
"schedule_handler_old",
"dynamic_skip"
"preprocessor"
]
}
}
+28
View File
@@ -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)
+53
View File
@@ -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<Utc>,
started_at: Option<chrono::DateTime<Utc>>,
duration_ms: Option<i64>,
}
async fn get_completed_job_timing(
OptAuthed(opt_authed): OptAuthed,
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
) -> error::JsonResult<JobTiming> {
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,
@@ -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)
@@ -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)