diff --git a/backend/openapi.yaml b/backend/openapi.yaml index 9705eb7a78..995b241e7f 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -2648,6 +2648,7 @@ paths: schema: type: string requestBody: + required: true content: application/json: schema: @@ -2710,6 +2711,7 @@ paths: schema: type: string requestBody: + required: true content: application/json: schema: @@ -2722,6 +2724,32 @@ paths: schema: type: string + /w/{workspace}/jobs/get_flow/{id}/{resume_id}/{signature}: + get: + summary: get parent flow job of suspended job + operationId: getSuspendedJobFlow + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/JobId" + - name: resume_id + in: path + required: true + schema: + type: integer + - name: signature + in: path + required: true + schema: + type: string + responses: + "200": + description: parent flow details + content: + application/json: + schema: + $ref: "#/components/schemas/Job" /schedules/preview: post: diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index a9ae4055c6..b8f3d94ff1 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -878,6 +878,27 @@ }, "query": "SELECT * FROM usr where username = $1 AND workspace_id = $2" }, + "3da0cf7edc975cb365c36d167df34d9d30d3e86f231bb9d1d3cc3bad6a50dfff": { + "describe": { + "columns": [ + { + "name": "parent_job", + "ordinal": 0, + "type_info": "Uuid" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + } + }, + "query": "\n SELECT parent_job\n FROM queue\n WHERE id = $1 AND workspace_id = $2\n UNION ALL\n SELECT parent_job\n FROM completed_job\n WHERE id = $1 AND workspace_id = $2\n " + }, "3fabb3857c9cf2d057814b54ee54a95d01b6a7d9e89bea239b832a9d70f0044b": { "describe": { "columns": [], diff --git a/backend/src/jobs.rs b/backend/src/jobs.rs index f0dcec3dfb..03204ef0dc 100644 --- a/backend/src/jobs.rs +++ b/backend/src/jobs.rs @@ -97,6 +97,10 @@ pub fn global_service() -> Router { "/cancel/:job_id/:resume_id/:secret", post(cancel_suspended_job), ) + .route( + "/get_flow/:job_id/:resume_id/:secret", + get(get_suspended_job_flow), + ) } #[derive(Debug, sqlx::FromRow, Serialize, Clone)] @@ -1100,6 +1104,40 @@ pub async fn cancel_suspended_job( Ok(StatusCode::CREATED) } +pub async fn get_suspended_job_flow( + /* unauthed */ + Extension(db): Extension, + Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>, +) -> error::JsonResult { + let mut tx = db.begin().await?; + let key = get_workspace_key(&w_id, &mut tx).await?; + let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(to_anyhow)?; + mac.update(job.as_bytes()); + mac.update(resume_id.to_be_bytes().as_ref()); + mac.verify_slice(hex::decode(secret)?.as_ref()) + .map_err(|_| anyhow::anyhow!("Invalid signature"))?; + let flow_id = sqlx::query_scalar!( + r#" + SELECT parent_job + FROM queue + WHERE id = $1 AND workspace_id = $2 + UNION ALL + SELECT parent_job + FROM completed_job + WHERE id = $1 AND workspace_id = $2 + "#, + job, + w_id + ) + .fetch_optional(&mut tx) + .await? + .flatten() + .ok_or_else(|| anyhow::anyhow!("parent flow job not found"))?; + let flow_o = get_job_by_id(tx, &w_id, flow_id).await?.0; + let flow = crate::utils::not_found_if_none(flow_o, "Parent Flow", job.to_string())?; + Ok(Json(flow)) +} + pub async fn create_job_signature( authed: Authed, Extension(user_db): Extension, diff --git a/deno-client/mod.ts b/deno-client/mod.ts index 40b03ec8ef..19f5a3b5fa 100644 --- a/deno-client/mod.ts +++ b/deno-client/mod.ts @@ -139,11 +139,11 @@ export async function databaseUrlFromResource(path: string): Promise { export async function genNounceAndHmac(workspace: string, jobId: string) { - const nounce = Math.floor(Math.random() * 4294967295); + const nonce = Math.floor(Math.random() * 4294967295); const sig = await fetch(Deno.env.get("WM_BASE_URL") + - `/api/w/${workspace}/jobs/job_signature/${jobId}/${nounce}?token=${Deno.env.get("WM_TOKEN")}`) + `/api/w/${workspace}/jobs/job_signature/${jobId}/${nonce}?token=${Deno.env.get("WM_TOKEN")}`) return { - nounce, + nonce, signature: await sig.text() }; } @@ -151,7 +151,7 @@ export async function genNounceAndHmac(workspace: string, jobId: string) { export async function getResumeEndpoints() { const workspace = getWorkspace() - const { nounce, signature } = await genNounceAndHmac( + const { nonce, signature } = await genNounceAndHmac( workspace, Deno.env.get("WM_JOB_ID") ?? "no_job_id", ); @@ -160,10 +160,11 @@ export async function getResumeEndpoints() { function getResumeUrl(op: string) { return url_prefix + - `${op}/${Deno.env.get("WM_JOB_ID")}/${nounce}/${signature}`; + `${op}/${Deno.env.get("WM_JOB_ID")}/${nonce}/${signature}`; } return { + approvalPage: Deno.env.get("WM_BASE_URL") + `/approve/${workspace}/${Deno.env.get("WM_JOB_ID")}/${nonce}/${signature}`, resume: getResumeUrl("resume"), cancel: getResumeUrl("cancel"), }; diff --git a/frontend/src/lib/components/FlowMetadata.svelte b/frontend/src/lib/components/FlowMetadata.svelte new file mode 100644 index 0000000000..e6d0d2b874 --- /dev/null +++ b/frontend/src/lib/components/FlowMetadata.svelte @@ -0,0 +1,56 @@ + + +
+ +
+ + Created {displayDaysAgo(job.created_at ?? '')} +
+ {#if job && 'started_at' in job && job.started_at} +
+ + Started {displayDaysAgo(job.started_at ?? '')} +
+ {/if} +
+ {#if job && job.parent_job} + {#if job.is_flow_step} + + Step of flow {job.parent_job} + {:else} + + Triggered by parent {job.parent_job} + {/if} + {:else if job && job.schedule_path} + + Triggered by the schedule: {job.schedule_path} + {/if} +
+ + By {job.created_by} + {#if job.permissioned_as !== `u/${job.created_by}`}but permissioned as {job.permissioned_as}{/if} + +
+
+
+ run id: {job.id} +
+
diff --git a/frontend/src/lib/components/flows/content/FlowInputs.svelte b/frontend/src/lib/components/flows/content/FlowInputs.svelte index e79ad952d0..e2b0c14ef6 100644 --- a/frontend/src/lib/components/flows/content/FlowInputs.svelte +++ b/frontend/src/lib/components/flows/content/FlowInputs.svelte @@ -16,7 +16,7 @@
-
Inline script
+
Common script
{/if} -
-
Pre-made script
-
+ import { Job, JobService } from '$lib/gen' + import { page } from '$app/stores' + import Button from '$lib/components/common/button/Button.svelte' + import CenteredModal from '$lib/components/CenteredModal.svelte' + import { sendUserToast } from '$lib/utils' + import FlowMetadata from '$lib/components/FlowMetadata.svelte' + + let job: Job | undefined = undefined + + getJob() + + async function getJob() { + job = await JobService.getSuspendedJobFlow({ + workspace: $page.params.workspace, + id: $page.params.job, + resumeId: new Number($page.params.resume).valueOf(), + signature: $page.params.hmac + }) + } + + async function resume() { + await JobService.resumeSuspendedJobPost({ + workspace: $page.params.workspace, + id: $page.params.job, + resumeId: new Number($page.params.resume).valueOf(), + signature: $page.params.hmac, + requestBody: {} + }) + sendUserToast('Flow approved') + } + + async function cancel() { + await JobService.cancelSuspendedJobPost({ + workspace: $page.params.workspace, + id: $page.params.job, + resumeId: new Number($page.params.resume).valueOf(), + signature: $page.params.hmac, + requestBody: {} + }) + sendUserToast('Flow disapproved!') + } + + +
+ + {#if job} + + {/if} + +
+ + +
+ + +
+
diff --git a/frontend/src/routes/run/[...run].svelte b/frontend/src/routes/run/[...run].svelte index 2c37c3cb53..afa9055c55 100644 --- a/frontend/src/routes/run/[...run].svelte +++ b/frontend/src/routes/run/[...run].svelte @@ -41,13 +41,13 @@ import { userStore, workspaceStore } from '$lib/stores' import CenteredPage from '$lib/components/CenteredPage.svelte' import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte' - import JobStatus from '$lib/components/JobStatus.svelte' import TableCustom from '$lib/components/TableCustom.svelte' import ArgInfo from '$lib/components/ArgInfo.svelte' import HighlightCode from '$lib/components/HighlightCode.svelte' import TestJobLoader from '$lib/components/TestJobLoader.svelte' import LogViewer from '$lib/components/LogViewer.svelte' import { Button, ActionRow, Skeleton } from '$lib/components/common' + import FlowMetadata from '$lib/components/FlowMetadata.svelte' let workspace_id_query: string | undefined = $page.url.searchParams.get('workspace') ?? undefined let workspace_id: string | undefined @@ -291,66 +291,7 @@
- {#if job} -
- -
- - Created {displayDaysAgo(job.created_at ?? '')} -
- {#if job && 'started_at' in job && job.started_at} -
- - Started {displayDaysAgo(job.started_at ?? '')} -
- {/if} -
- {#if job && job.parent_job} - {#if job.is_flow_step} - - Step of flow {job.parent_job} - {:else} - - Triggered by parent {job.parent_job} - {/if} - {:else if job && job.schedule_path} - - Triggered by the schedule: {job.schedule_path} - {/if} -
- - By {job.created_by} - {#if job.permissioned_as !== `u/${job.created_by}`}but permissioned as {job.permissioned_as}{/if} - -
-
-
- run id: {job.id} -
-
- {/if} + {#if job}{/if}