fix: improve approval flow with approval page

This commit is contained in:
Ruben Fiszel
2022-10-22 13:55:12 +02:00
parent f8fce1fa78
commit a9359dada7
8 changed files with 210 additions and 70 deletions
+28
View File
@@ -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:
+21
View File
@@ -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": [],
+38
View File
@@ -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
View File
@@ -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>
+2 -61
View File
@@ -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>