mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 08:01:38 +00:00
feat: allow downloading args over the size limit
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "bool",
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5718,6 +5718,23 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/jobs_u/get_args/{id}:
|
||||
get:
|
||||
summary: get job args
|
||||
operationId: getJobArgs
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
responses:
|
||||
"200":
|
||||
description: job args
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/jobs_u/getupdate/{id}:
|
||||
get:
|
||||
summary: get job updates
|
||||
|
||||
@@ -266,6 +266,7 @@ pub fn global_service() -> Router {
|
||||
.route("/get_root_job_id/:id", get(get_root_job))
|
||||
.route("/get/:id", get(get_job))
|
||||
.route("/get_logs/:id", get(get_job_logs))
|
||||
.route("/get_args/:id", get(get_args))
|
||||
.route("/get_flow_debug_info/:id", get(get_flow_job_debug_info))
|
||||
.route("/completed/get/:id", get(get_completed_job))
|
||||
.route("/completed/get_result/:id", get(get_completed_job_result))
|
||||
@@ -906,6 +907,58 @@ async fn get_job_logs(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct RawArgs {
|
||||
pub args: Option<sqlx::types::Json<Box<RawValue>>>,
|
||||
pub created_by: String,
|
||||
}
|
||||
|
||||
async fn get_args(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::JsonResult<Box<serde_json::value::RawValue>> {
|
||||
let record = sqlx::query(
|
||||
"SELECT created_by, args
|
||||
FROM completed_job
|
||||
WHERE completed_job.id = $1 AND completed_job.workspace_id = $2",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
if let Some(record) = record {
|
||||
let record = RawArgs::from_row(&record)
|
||||
.map_err(|e| Error::InternalErr(format!("error parsing args: {e:#}")))?;
|
||||
if opt_authed.is_none() && record.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(record.args.map(|x| x.0).unwrap_or_default()))
|
||||
} else {
|
||||
let record = sqlx::query(
|
||||
"SELECT created_by, args
|
||||
FROM queue
|
||||
WHERE queue.id = $1 AND queue.workspace_id = $2",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
let record = not_found_if_none(record, "Job Args", id.to_string())?;
|
||||
let record = RawArgs::from_row(&record)
|
||||
.map_err(|e| Error::InternalErr(format!("error parsing args: {e:#}")))?;
|
||||
if opt_authed.is_none() && record.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(record.args.map(|x| x.0).unwrap_or_default()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, sqlx::FromRow, Serialize)]
|
||||
pub struct ListableCompletedJob {
|
||||
pub r#type: String,
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
if (modId) {
|
||||
if ($flowStateStore && $flowStateStore?.[modId] == undefined) {
|
||||
$flowStateStore[modId] = {
|
||||
...$flowStateStore[modId],
|
||||
...($flowStateStore[modId] ?? {}),
|
||||
previewResult: jobLoaded.args
|
||||
}
|
||||
}
|
||||
@@ -854,7 +854,11 @@
|
||||
{:else if selectedNode == 'start'}
|
||||
{#if job.args}
|
||||
<div class="p-2">
|
||||
<JobArgs args={job.args} />
|
||||
<JobArgs
|
||||
id={job.id}
|
||||
workspace={job.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={job.args}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="p-2 text-secondary">No arguments</p>
|
||||
@@ -883,7 +887,11 @@
|
||||
</div>
|
||||
{#if !node.isListJob}
|
||||
<div class="px-1 py-1">
|
||||
<JobArgs args={node.args} />
|
||||
<JobArgs
|
||||
id={node.job_id}
|
||||
workspace={job.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={node.args}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<FlowJobResult
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
import Head from './table/Head.svelte'
|
||||
import Row from './table/Row.svelte'
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let id: string | undefined = undefined
|
||||
export let args: any
|
||||
export let argLabel: string | undefined = undefined
|
||||
export let workspace: string | undefined = undefined
|
||||
|
||||
let jsonViewer: Drawer
|
||||
let runLocally: Drawer
|
||||
@@ -49,48 +52,55 @@ ${Object.entries(args)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<DataTable size="sm">
|
||||
<Head>
|
||||
<tr>
|
||||
<Cell head first>{argLabel ?? 'Arg'}</Cell>
|
||||
<Cell head last>Value</Cell>
|
||||
</tr>
|
||||
<svelte:fragment slot="headerAction">
|
||||
<button
|
||||
on:click={() => {
|
||||
jsonStr = JSON.stringify(args, null, 4)
|
||||
jsonViewer.openDrawer()
|
||||
}}
|
||||
>
|
||||
<Expand size={18} />
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</Head>
|
||||
{#if id && workspace && args && typeof args === 'object' && deepEqual( Object.keys(args), ['reason'] ) && args['reason'] == 'WINDMILL_TOO_BIG'}
|
||||
The args are too big in size to be able to fetch s3. Please <a
|
||||
href="/api/w/{workspace}/jobs_u/get_args/{id}"
|
||||
target="_blank">download the JSON file to view them</a
|
||||
>.
|
||||
{:else}
|
||||
<div class="relative">
|
||||
<DataTable size="sm">
|
||||
<Head>
|
||||
<tr>
|
||||
<Cell head first>{argLabel ?? 'Arg'}</Cell>
|
||||
<Cell head last>Value</Cell>
|
||||
</tr>
|
||||
<svelte:fragment slot="headerAction">
|
||||
<button
|
||||
on:click={() => {
|
||||
jsonStr = JSON.stringify(args, null, 4)
|
||||
jsonViewer.openDrawer()
|
||||
}}
|
||||
>
|
||||
<Expand size={18} />
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</Head>
|
||||
|
||||
<tbody class="divide-y">
|
||||
{#if args && Object.keys(args).length > 0}
|
||||
{#each Object.entries(args).sort((a, b) => a[0].localeCompare(b[0])) as [arg, value]}
|
||||
<tbody class="divide-y">
|
||||
{#if args && Object.keys(args).length > 0}
|
||||
{#each Object.entries(args).sort((a, b) => a[0].localeCompare(b[0])) as [arg, value]}
|
||||
<Row>
|
||||
<Cell first>{arg}</Cell>
|
||||
<Cell last><ArgInfo {value} /></Cell>
|
||||
</Row>
|
||||
{/each}
|
||||
{:else if args}
|
||||
<Row><Cell>No arguments</Cell></Row>
|
||||
{:else}
|
||||
<Row>
|
||||
<Cell first>{arg}</Cell>
|
||||
<Cell last><ArgInfo {value} /></Cell>
|
||||
<Cell first>
|
||||
<Skeleton layout={[[1], 0.5, [1]]} />
|
||||
</Cell>
|
||||
<Cell last>
|
||||
<Skeleton layout={[[1], 0.5, [1]]} />
|
||||
</Cell>
|
||||
</Row>
|
||||
{/each}
|
||||
{:else if args}
|
||||
<Row><Cell>No arguments</Cell></Row>
|
||||
{:else}
|
||||
<Row>
|
||||
<Cell first>
|
||||
<Skeleton layout={[[1], 0.5, [1]]} />
|
||||
</Cell>
|
||||
<Cell last>
|
||||
<Skeleton layout={[[1], 0.5, [1]]} />
|
||||
</Cell>
|
||||
</Row>
|
||||
{/if}
|
||||
</tbody>
|
||||
</DataTable>
|
||||
</div>
|
||||
{/if}
|
||||
</tbody>
|
||||
</DataTable>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
@@ -99,7 +109,9 @@ ${Object.entries(args)
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
download="windmill-args.json"
|
||||
href="data:text/json;charset=utf-8,{encodeURIComponent(jsonStr)}"
|
||||
href={id && workspace
|
||||
? `/api/w/${workspace}/jobs_u/get_args/${id}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(jsonStr)}`}
|
||||
startIcon={{ icon: Download }}
|
||||
size="xs"
|
||||
color="light"
|
||||
@@ -123,14 +135,17 @@ ${Object.entries(args)
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{#if jsonStr.length > 100000}
|
||||
{#if jsonStr.length > 100000 || (id && workspace && args && typeof args === 'object' && deepEqual( Object.keys(args), ['reason'] ) && args['reason'] == 'WINDMILL_TOO_BIG')}
|
||||
<div class="text-sm mb-2 text-tertiary">
|
||||
<a
|
||||
download="windmill-args.json"
|
||||
href="data:text/json;charset=utf-8,{encodeURIComponent(jsonStr)}">Download</a
|
||||
href={id && workspace
|
||||
? `/api/w/${workspace}/jobs_u/get_args/${id}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(jsonStr)}`}
|
||||
>
|
||||
JSON is too large to be displayed in full.
|
||||
</div>
|
||||
JSON is too large to be displayed in full.
|
||||
</a></div
|
||||
>
|
||||
{:else}
|
||||
<Highlight language={json} code={jsonStr.replace(/\\n/g, '\n')} />
|
||||
{/if}
|
||||
|
||||
@@ -68,10 +68,21 @@
|
||||
export let isValid = true
|
||||
|
||||
$: onArgsChange(args)
|
||||
let debounced: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
function onArgsChange(args: any) {
|
||||
try {
|
||||
window.location.hash = computeSharableHash(args)
|
||||
debounced && clearTimeout(debounced)
|
||||
debounced = setTimeout(() => {
|
||||
const nurl = new URL(window.location.href)
|
||||
nurl.hash = computeSharableHash(args)
|
||||
|
||||
try {
|
||||
history.replaceState(history.state, '', nurl.toString())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, 200)
|
||||
} catch (e) {
|
||||
console.error('Impossible to set hash in args', e)
|
||||
}
|
||||
|
||||
@@ -1125,7 +1125,11 @@
|
||||
{/if}
|
||||
{#if job?.args}
|
||||
<div class="p-2">
|
||||
<JobArgs args={job?.args} />
|
||||
<JobArgs
|
||||
id={job.id}
|
||||
workspace={job.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={job?.args}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job?.raw_code}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { Badge } from '../common'
|
||||
import { forLater } from '$lib/forLater'
|
||||
import DurationMs from '../DurationMs.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
const POPUP_HEIGHT = 320 as const
|
||||
|
||||
@@ -108,7 +109,11 @@
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
{#if job?.['duration_ms']}
|
||||
<DurationMs duration_ms={job?.['duration_ms']} self_wait_time_ms={job?.self_wait_time_ms} aggregate_wait_time_ms={job?.aggregate_wait_time_ms} />
|
||||
<DurationMs
|
||||
duration_ms={job?.['duration_ms']}
|
||||
self_wait_time_ms={job?.self_wait_time_ms}
|
||||
aggregate_wait_time_ms={job?.aggregate_wait_time_ms}
|
||||
/>
|
||||
{/if}
|
||||
{#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0}
|
||||
{#each job?.['labels'] as label}
|
||||
@@ -117,7 +122,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-auto">
|
||||
<JobArgs args={job?.args} />
|
||||
<JobArgs
|
||||
id={job?.id}
|
||||
workspace={job?.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={job?.args}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-auto p-2">
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
|
||||
@@ -133,7 +133,11 @@
|
||||
<span class="font-semibold text-xs leading-6">Arguments</span>
|
||||
|
||||
<div class="w-full">
|
||||
<JobArgs args={job?.args} />
|
||||
<JobArgs
|
||||
id={job?.id}
|
||||
workspace={job?.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={job?.args}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if job?.type === 'CompletedJob'}
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
|
||||
let hash = window.location.hash
|
||||
if (hash.length > 1) {
|
||||
console.log(hash)
|
||||
try {
|
||||
let searchParams = new URLSearchParams(hash.slice(1))
|
||||
let params = [...searchParams.entries()].map(([k, v]) => [k, JSON.parse(v)])
|
||||
|
||||
@@ -697,7 +697,11 @@
|
||||
class="flex flex-col gap-y-8 sm:grid sm:grid-cols-3 sm:gap-10 max-w-7xl mx-auto w-full px-4"
|
||||
>
|
||||
<div class="col-span-2">
|
||||
<JobArgs args={job?.args} />
|
||||
<JobArgs
|
||||
workspace={job?.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
id={job?.id}
|
||||
args={job?.args}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton loading={!job} layout={[[9.5]]} />
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
let args: Record<string, any> | undefined = undefined
|
||||
let hash = window.location.hash
|
||||
if (hash.length > 1) {
|
||||
console.log(hash)
|
||||
try {
|
||||
let searchParams = new URLSearchParams(hash.slice(1))
|
||||
let params = [...searchParams.entries()].map(([k, v]) => [k, JSON.parse(v)])
|
||||
|
||||
@@ -207,7 +207,11 @@
|
||||
{#if !completed}
|
||||
<h2 class="mt-4 mb-2">Flow arguments</h2>
|
||||
|
||||
<JobArgs args={job?.args} />
|
||||
<JobArgs
|
||||
id={job?.id}
|
||||
workspace={job?.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={job?.args}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="mt-8">
|
||||
|
||||
Reference in New Issue
Block a user