feat: allow downloading args over the size limit

This commit is contained in:
Ruben Fiszel
2024-07-02 17:54:19 +02:00
parent 14bacba0ad
commit d13a357b26
13 changed files with 184 additions and 57 deletions
@@ -5,7 +5,7 @@
"columns": [
{
"ordinal": 0,
"name": "bool",
"name": "?column?",
"type_info": "Bool"
}
],
+17
View File
@@ -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
+53
View File
@@ -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
+59 -44
View File
@@ -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}
+12 -1
View File
@@ -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">