mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
fix: improve approval flow with approval page
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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<DB>,
|
||||
Path((w_id, job, resume_id, secret)): Path<(String, Uuid, u32, String)>,
|
||||
) -> error::JsonResult<Job> {
|
||||
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<UserDB>,
|
||||
|
||||
+6
-5
@@ -139,11 +139,11 @@ export async function databaseUrlFromResource(path: string): Promise<string> {
|
||||
|
||||
|
||||
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"),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { Job } from '$lib/gen'
|
||||
import JobStatus from '$lib/components/JobStatus.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { displayDaysAgo } from '$lib/utils'
|
||||
import { faCalendar, faClock, faRobot, faUser, faWind } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
export let job: Job
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
</script>
|
||||
|
||||
<div class="rounded-md p-3 bg-gray-50 shadow-sm sm:text-sm md:text-base" style="min-height: 150px;">
|
||||
<JobStatus {job} />
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
Created {displayDaysAgo(job.created_at ?? '')}</span
|
||||
>
|
||||
</div>
|
||||
{#if job && 'started_at' in job && job.started_at}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
Started {displayDaysAgo(job.started_at ?? '')}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if job && job.parent_job}
|
||||
{#if job.is_flow_step}
|
||||
<Icon class="text-gray-700" data={faWind} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
Step of flow <a href={`/run/${job.parent_job}`}>{job.parent_job}</a></span
|
||||
>
|
||||
{:else}
|
||||
<Icon class="text-gray-700" data={faRobot} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
Triggered by parent <a href={`/run/${job.parent_job}`}>{job.parent_job}</a></span
|
||||
>
|
||||
{/if}
|
||||
{:else if job && job.schedule_path}
|
||||
<Icon class="text-gray-700" data={faCalendar} scale={SMALL_ICON_SCALE} />
|
||||
<span class="mx-2"
|
||||
>Triggered by the schedule: <a
|
||||
href={`/schedule/add?edit=${job.schedule_path}&isFlow=${job.job_kind == 'flow'}`}
|
||||
>{job.schedule_path}</a
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faUser} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
By {job.created_by}
|
||||
{#if job.permissioned_as !== `u/${job.created_by}`}but permissioned as {job.permissioned_as}{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-700 text-2xs pt-2">
|
||||
run id: <a href={`/run/${job.id}`}>{job.id}</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="text-sm font-bold">Inline script</div>
|
||||
<div class="text-sm font-bold">Common script</div>
|
||||
<div class="grid sm:grid-col-2 lg:grid-cols-3 gap-4">
|
||||
<FlowScriptPicker
|
||||
label="Inline Python (3.10)"
|
||||
@@ -63,10 +63,7 @@
|
||||
dispatch('new', { language: RawScript.language.DENO, kind: 'script', subkind: 'pgsql' })}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-sm font-bold">Pre-made script</div>
|
||||
|
||||
<div class="grid sm:grid-col-2 lg:grid-cols-3 gap-4">
|
||||
<PickScript
|
||||
customText={failureModule ? 'Error Handler from workspace' : undefined}
|
||||
kind={failureModule ? Script.kind.FAILURE : Script.kind.SCRIPT}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
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!')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen antialiased text-gray-900">
|
||||
<CenteredModal title="Approve flow?">
|
||||
{#if job}
|
||||
<FlowMetadata {job} />
|
||||
{/if}
|
||||
|
||||
<div class="w-max-md flex flex-row gap-x-4 gap-y-4 justify-between w-full flex-wrap">
|
||||
<Button btnClasses="grow" color="red" on:click={cancel} size="md">Disapprove/Cancel</Button>
|
||||
<Button btnClasses="grow" on:click={resume} size="md">Approve/Resume</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4"><a href="https://windmill.dev">Learn more about Windmill</a></div>
|
||||
</CenteredModal>
|
||||
</div>
|
||||
@@ -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 @@
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton loading={!job} layout={[[9.5]]} />
|
||||
{#if job}
|
||||
<div
|
||||
class="rounded-md p-3 bg-gray-50 shadow-sm sm:text-sm md:text-base"
|
||||
style="min-height: 150px;"
|
||||
>
|
||||
<JobStatus {job} />
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
Created {displayDaysAgo(job.created_at ?? '')}</span
|
||||
>
|
||||
</div>
|
||||
{#if job && 'started_at' in job && job.started_at}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
Started {displayDaysAgo(job.started_at ?? '')}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if job && job.parent_job}
|
||||
{#if job.is_flow_step}
|
||||
<Icon class="text-gray-700" data={faWind} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
Step of flow <a href={`/run/${job.parent_job}`}>{job.parent_job}</a></span
|
||||
>
|
||||
{:else}
|
||||
<Icon class="text-gray-700" data={faRobot} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
Triggered by parent <a href={`/run/${job.parent_job}`}>{job.parent_job}</a></span
|
||||
>
|
||||
{/if}
|
||||
{:else if job && job.schedule_path}
|
||||
<Icon class="text-gray-700" data={faCalendar} scale={SMALL_ICON_SCALE} />
|
||||
<span class="mx-2"
|
||||
>Triggered by the schedule: <a
|
||||
href={`/schedule/add?edit=${job.schedule_path}&isFlow=${job.job_kind == 'flow'}`}
|
||||
>{job.schedule_path}</a
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faUser} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
By {job.created_by}
|
||||
{#if job.permissioned_as !== `u/${job.created_by}`}but permissioned as {job.permissioned_as}{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-700 text-2xs pt-2">
|
||||
run id: <a href={`/run/${job.id}`}>{job.id}</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job}<FlowMetadata {job} />{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user