pass workspace as query arg of every runs link

This commit is contained in:
Ruben Fiszel
2022-11-08 10:27:25 +01:00
parent d969eb4f2c
commit 1cefbe263b
10 changed files with 283 additions and 217 deletions
+4 -1
View File
@@ -710,7 +710,10 @@ async fn slack_command(
.await?;
tx.commit().await?;
let url = base_url.0.to_owned();
return Ok(format!("Job launched. See details at {url}/run/{uuid}"));
return Ok(format!(
"Job launched. See details at {url}/run/{uuid}?workspace={}",
&settings.workspace_id
));
}
}
@@ -71,6 +71,6 @@
</div>
</div>
<div class="text-gray-700 text-2xs pt-2">
run id: <a href={`/run/${job.id}`}>{job.id}</a>
run id: <a href={`/run/${job.id}?workspace=${job.workspace_id}`}>{job.id}</a>
</div>
</div>
+22 -2
View File
@@ -16,6 +16,7 @@
} from 'chart.js'
import type { CompletedJob } from '$lib/gen'
import { createEventDispatcher } from 'svelte'
import { json } from 'svelte-highlight/languages'
export let jobs: CompletedJob[] | undefined = []
@@ -43,13 +44,16 @@
// borderColor: 'rgba(99,0,125, .2)',
backgroundColor: '#f87171',
label: 'Failed',
data: failed?.map((job) => ({ x: job.created_at as any, y: job.duration_ms })) ?? []
data:
failed?.map((job) => ({ x: job.created_at as any, y: job.duration_ms, id: job.id })) ?? []
},
{
// borderColor: 'rgba(99,0,125, .2)',
backgroundColor: '#4ade80',
label: 'Successful',
data: success?.map((job) => ({ x: job.created_at as any, y: job.duration_ms })) ?? []
data:
success?.map((job) => ({ x: job.created_at as any, y: job.duration_ms, id: job.id })) ??
[]
}
]
}
@@ -83,8 +87,24 @@
zoom: zoomOptions,
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
let label = context.dataset.label || 'X'
if (label) {
label += ': '
}
if (context.parsed.y !== null) {
label += JSON.stringify(context.parsed)
}
return label
}
}
}
},
scales: {
x: {
grid: {
@@ -11,7 +11,10 @@
export let isLoading = false
export let job: Job | undefined = undefined
export let workspaceOverride: string | undefined = undefined
export let notfound = false
$: workspace = workspaceOverride ?? $workspaceStore
let intervalId: NodeJS.Timer
let syncIteration: number = 0
@@ -27,7 +30,7 @@
intervalId && clearInterval(intervalId)
if (isLoading && job) {
JobService.cancelQueuedJob({
workspace: $workspaceStore!,
workspace: workspace!,
id: job.id,
requestBody: {}
})
@@ -35,7 +38,7 @@
isLoading = true
const testId = await JobService.runScriptPreview({
workspace: $workspaceStore!,
workspace: workspace!,
requestBody: {
path,
content: code,
@@ -78,7 +81,7 @@
try {
if (job && `running` in job) {
let previewJobUpdates = await JobService.getJobUpdates({
workspace: $workspaceStore!,
workspace: workspace!,
id,
running: job.running,
logOffset: job.logs?.length ?? 0
@@ -88,21 +91,29 @@
job.logs = (job.logs ?? '').concat(previewJobUpdates.new_logs)
}
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
job = await JobService.getJob({ workspace: $workspaceStore!, id })
job = await JobService.getJob({ workspace: workspace!, id })
}
} else {
job = await JobService.getJob({ workspace: $workspaceStore!, id })
console.log(workspaceOverride)
console.log(workspace)
job = await JobService.getJob({ workspace: workspace!, id })
}
if (job?.type === 'CompletedJob') {
//only CompletedJob has success property
isCompleted = true
clearInterval(intervalId)
intervalId && clearInterval(intervalId)
if (isLoading) {
dispatch('done', job)
isLoading = false
}
}
notfound = false
} catch (err) {
intervalId && clearInterval(intervalId)
if (err.status === 404) {
notfound = true
}
console.error(err)
}
return isCompleted
@@ -89,13 +89,15 @@
<div class="flex flex-row space-x-2">
<div class="whitespace-nowrap">
{#if job.script_path}
<a href="/run/{job.id}">{job.script_path} </a>
<a href="/run/{job.id}?workspace={job.workspace_id}">{job.script_path} </a>
{:else if 'job_kind' in job && job.job_kind == 'preview'}
<a href="/run/{job.id}">Preview without path </a>
<a href="/run/{job.id}?workspace={job.workspace_id}">Preview without path </a>
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
<a href="/run/{job.id}">lock deps of {truncateHash(job.script_hash ?? '')}</a>
<a href="/run/{job.id}?workspace={job.workspace_id}"
>lock deps of {truncateHash(job.script_hash ?? '')}</a
>
{:else if 'job_kind' in job && job.job_kind == 'identity'}
<a href="/run/{job.id}">no op</a>
<a href="/run/{job.id}?workspace={job.workspace_id}">no op</a>
{/if}
<button
class="ml-1"
@@ -109,8 +111,10 @@
><Badge color="blue">{job.job_kind}</Badge></a
>
{/if}
{#if (job.is_flow_step && job.job_kind == 'script') || job.job_kind == 'preview'}
<a href="/run/{job.parent_job}"><Badge color="gray">flow step</Badge></a>
{#if job.is_flow_step && (job.job_kind == 'script' || job.job_kind == 'preview')}
<a href="/run/{job.parent_job}?workspace={job.workspace_id}"
><Badge color="gray">flow step</Badge></a
>
{/if}
</div>
</div>
@@ -20,7 +20,7 @@
Job Id
</th>
<td class="py-2">
<a rel="noreferrer" target="_blank" href="/run/{job?.id}">
<a rel="noreferrer" target="_blank" href="/run/{job?.id}?workspace={job?.workspace_id}">
{job?.id}
</a>
</td>
@@ -161,7 +161,9 @@
<div class="mt-4 flex flex-row flex-wrap justify-between"
><a href="https://windmill.dev">Learn more about Windmill</a>
<a target="_blank" href="/run/{job?.id}">Flow run details (require auth)</a>
<a target="_blank" rel="noreferrer" href="/run/{job?.id}?workspace={job?.workspace_id}"
>Flow run details (require auth)</a
>
</div>
{#if job && job.raw_flow}
<h2 class="mt-10">Flow details</h2>
@@ -54,8 +54,8 @@
requestBody: args,
scheduledFor
})
sendUserToast(`Job <a href='/run/${run}'>${run}</a> started`)
goto('/run/' + run)
sendUserToast(`Job <a href='/run/${run}?workspace=${$workspaceStore}'>${run}</a> started`)
goto('/run/' + run + '?workspace=' + $workspaceStore)
}
$: {
+221 -195
View File
@@ -26,7 +26,7 @@
faFastForward
} from '@fortawesome/free-solid-svg-icons'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
@@ -39,8 +39,7 @@
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
let workspace_id_query: string | undefined = $page.url.searchParams.get('workspace') ?? undefined
let workspace_id: string | undefined
$: workspace_id = $page.url.searchParams.get('workspace') ?? $workspaceStore
let job: Job | undefined
const iconScale = 1
@@ -83,11 +82,11 @@
}
$: {
if ($workspaceStore && $page.params.run && testJobLoader) {
workspace_id = workspace_id_query ?? $workspaceStore
if (workspace_id && $page.params.run && testJobLoader) {
getLogs()
}
}
let notfound = false
</script>
<TestJobLoader
@@ -95,205 +94,232 @@
bind:this={testJobLoader}
bind:isLoading={testIsLoading}
bind:job
workspaceOverride={workspace_id}
bind:notfound
/>
<Skeleton
class="!max-w-6xl !px-4 sm:!px-6 md:!px-8"
loading={!job}
layout={[0.75, [2, 0, 2], 2.25, [{ h: 1.5, w: 40 }]]}
/>
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
<ActionRow applyPageWidth stickToTop>
<svelte:fragment slot="left">
{@const isScript = job?.job_kind === 'script'}
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
{#if job && 'deleted' in job && !job?.deleted && ($userStore?.is_admin ?? false)}
{#if notfound}
<CenteredPage>
<div class="flex flex-col gap-6">
<h1 class="text-red-400 mt-6">Job {$page.params.run} not found in {workspace_id}</h1>
<h2>Are you in the right workspace?</h2>
<div class="flex flex-col gap-2">
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
<div
><Button variant="border" href="/run/{$page.params.run}?workspace={workspace.id}"
>See in {workspace.name}</Button
></div
>
{/each}
<div>
<Button href="/runs">Go to runs page</Button>
</div>
</div>
</div>
</CenteredPage>
{:else}
<Skeleton
class="!max-w-6xl !px-4 sm:!px-6 md:!px-8"
loading={!job}
layout={[0.75, [2, 0, 2], 2.25, [{ h: 1.5, w: 40 }]]}
/>
{#if job?.job_kind === 'script' || job?.job_kind === 'flow'}
<ActionRow applyPageWidth stickToTop>
<svelte:fragment slot="left">
{@const isScript = job?.job_kind === 'script'}
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
{#if job && 'deleted' in job && !job?.deleted && ($userStore?.is_admin ?? false)}
<Button
variant="border"
color="red"
size="xs"
startIcon={{ icon: faTrash }}
on:click={() => job?.id && deleteCompletedJob(job.id)}
>
Delete
</Button>
<Button
href={runsHref}
variant="border"
color="blue"
size="xs"
startIcon={{ icon: faList }}
>
View runs
</Button>
{/if}
</svelte:fragment>
<svelte:fragment slot="right">
{@const stem = `/${job?.job_kind}s`}
{@const isScript = job?.job_kind === 'script'}
{@const route = isScript ? job?.script_hash : job?.script_path}
{@const runHref = `${stem}/run/${route}${
job?.args ? '?args=' + encodeURIComponent(encodeState(job?.args)) : ''
}`}
{@const editHref = `${stem}/edit/${route}${
isScript
? `?step=2${job?.args ? `&args=${encodeURIComponent(encodeState(job?.args))}` : ''}`
: `${job?.args ? `?args=${encodeURIComponent(encodeState(job?.args))}` : ''}`
}`}
{@const isRunning = job && 'running' in job && job.running}
{@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`}
{#if isRunning}
<Button
color="red"
size="xs"
startIcon={{ icon: faTimesCircle }}
on:click|once={() => {
if (job?.id) {
cancelJob(job?.id)
}
}}
>
Cancel
</Button>
{/if}
<Button
variant="border"
color="red"
size="xs"
startIcon={{ icon: faTrash }}
on:click={() => job?.id && deleteCompletedJob(job.id)}
>
Delete
</Button>
<Button
href={runsHref}
variant="border"
href={runHref}
disabled={isRunning}
color="blue"
size="xs"
startIcon={{ icon: faList }}
startIcon={{ icon: faRefresh }}>Run again</Button
>
View runs
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
<Button href={editHref} color="blue" size="xs" startIcon={{ icon: faEdit }}>Edit</Button>
{/if}
<Button href={viewHref} color="blue" size="xs" startIcon={{ icon: faScroll }}>
View {job?.job_kind}
</Button>
{/if}
</svelte:fragment>
<svelte:fragment slot="right">
{@const stem = `/${job?.job_kind}s`}
{@const isScript = job?.job_kind === 'script'}
{@const route = isScript ? job?.script_hash : job?.script_path}
{@const runHref = `${stem}/run/${route}${
job?.args ? '?args=' + encodeURIComponent(encodeState(job?.args)) : ''
}`}
{@const editHref = `${stem}/edit/${route}${
isScript
? `?step=2${job?.args ? `&args=${encodeURIComponent(encodeState(job?.args))}` : ''}`
: `${job?.args ? `?args=${encodeURIComponent(encodeState(job?.args))}` : ''}`
}`}
{@const isRunning = job && 'running' in job && job.running}
{@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`}
{@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`}
{#if isRunning}
<Button
color="red"
size="xs"
startIcon={{ icon: faTimesCircle }}
on:click|once={() => {
if (job?.id) {
cancelJob(job?.id)
}
}}
>
Cancel
</Button>
{/if}
<Button
href={runHref}
disabled={isRunning}
color="blue"
size="xs"
startIcon={{ icon: faRefresh }}>Run again</Button
>
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
<Button href={editHref} color="blue" size="xs" startIcon={{ icon: faEdit }}>Edit</Button>
{/if}
<Button href={viewHref} color="blue" size="xs" startIcon={{ icon: faScroll }}>
View {job?.job_kind}
</Button>
</svelte:fragment>
</ActionRow>
{/if}
<CenteredPage>
<h1 class="flex flex-row flex-wrap justify-between items-center gap-4 py-6">
<div>
{#if job}
{#if 'success' in job && job.success}
{#if job.is_skipped}
<Icon
class="text-green-600"
data={faFastForward}
scale={SMALL_ICON_SCALE}
label="Job completed successfully but was skipped"
/>
{:else}
<Icon
class="text-green-600"
data={check}
scale={SMALL_ICON_SCALE}
label="Job completed successfully"
/>
{/if}
{:else if job && 'success' in job}
<Icon
class="text-red-700"
data={faTimes}
scale={iconScale}
label="Job completed with an error"
/>
{:else if job && 'running' in job && job.running}
<Icon class="text-yellow-500" data={faCircle} scale={iconScale} label="Job is running" />
{:else if job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for)}
<Icon
class="text-gray-700"
data={faCalendar}
scale={iconScale}
label="Job is scheduled for a later time"
/>
{:else if job && 'running' in job && job.scheduled_for}
<Icon
class="text-gray-500"
data={faHourglassHalf}
scale={iconScale}
label="Job is waiting for an executor"
/>
{/if}
{job.script_path ?? (job.job_kind == 'dependencies' ? 'lock dependencies' : 'No path')}
{#if job.script_hash}
<a href="/scripts/get/{job.script_hash}"
><Badge color="gray">{truncateHash(job.script_hash)}</Badge></a
>
{/if}
{#if job && 'job_kind' in job}<Badge color="blue">{job.job_kind}</Badge>
{/if}
{/if}
</div>
</h1>
{#if job && 'deleted' in job && job?.deleted}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4" role="alert">
<p class="font-bold">Deleted</p>
<p>The content of this run was deleted (by an admin, no less)</p>
</div>
</svelte:fragment>
</ActionRow>
{/if}
<!-- Arguments and actions -->
<div class="flex flex-col mr-2 sm:mr-0 sm:grid sm:grid-cols-3 sm:gap-5">
<div class="col-span-2">
<JobArgs args={job?.args} />
{#if job?.job_kind == 'flow' || job?.job_kind == 'flowpreview'}
<div class="mt-10" />
<FlowProgressBar {job} class="py-4" />
<div class="max-w-lg">
<FlowStatusViewer
jobId={job.id}
on:jobsLoaded={({ detail }) => {
job = detail
}}
/>
</div>
{/if}
</div>
<div>
<Skeleton loading={!job} layout={[[9.5]]} />
{#if job}<FlowMetadata {job} />{/if}
</div>
</div>
{#if job?.job_kind !== 'flow' && job?.job_kind !== 'flowpreview'}
<!-- Logs and outputs-->
<div class="mr-2 sm:mr-0 mt-12">
<Tabs bind:selected={viewTab}>
<Tab value="result">Result</Tab>
<Tab value="logs">Logs</Tab>
{#if job?.job_kind == 'dependencies'}
<Tab value="code">Dependencies</Tab>
{:else if job?.job_kind == 'preview'}
<Tab value="code">Code</Tab>
{/if}
</Tabs>
<Skeleton loading={!job} layout={[[5]]} />
{#if job}
<div class="flex flex-row border rounded-md p-3 max-h-1/2 overflow-auto">
{#if viewTab == 'logs'}
<div class="w-full">
<LogViewer isLoading={!(job && 'logs' in job && job.logs)} content={job?.logs} />
</div>
{:else if viewTab == 'code'}
{#if job && 'raw_code' in job && job.raw_code}
<HighlightCode language={job.language} code={job.raw_code} />
{:else if job}
No code is available
<CenteredPage>
<h1 class="flex flex-row flex-wrap justify-between items-center gap-4 py-6">
<div>
{#if job}
{#if 'success' in job && job.success}
{#if job.is_skipped}
<Icon
class="text-green-600"
data={faFastForward}
scale={SMALL_ICON_SCALE}
label="Job completed successfully but was skipped"
/>
{:else}
<Skeleton layout={[[5]]} />
<Icon
class="text-green-600"
data={check}
scale={SMALL_ICON_SCALE}
label="Job completed successfully"
/>
{/if}
{:else if job !== undefined && 'result' in job && job.result !== undefined}
<DisplayResult result={job.result} />
{:else if job}
No output is available yet
{:else if job && 'success' in job}
<Icon
class="text-red-700"
data={faTimes}
scale={iconScale}
label="Job completed with an error"
/>
{:else if job && 'running' in job && job.running}
<Icon
class="text-yellow-500"
data={faCircle}
scale={iconScale}
label="Job is running"
/>
{:else if job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for)}
<Icon
class="text-gray-700"
data={faCalendar}
scale={iconScale}
label="Job is scheduled for a later time"
/>
{:else if job && 'running' in job && job.scheduled_for}
<Icon
class="text-gray-500"
data={faHourglassHalf}
scale={iconScale}
label="Job is waiting for an executor"
/>
{/if}
</div>
{/if}
{job.script_path ?? (job.job_kind == 'dependencies' ? 'lock dependencies' : 'No path')}
{#if job.script_hash}
<a href="/scripts/get/{job.script_hash}"
><Badge color="gray">{truncateHash(job.script_hash)}</Badge></a
>
{/if}
{#if job && 'job_kind' in job}<Badge color="blue">{job.job_kind}</Badge>
{/if}
{/if}
</div>
</h1>
{#if job && 'deleted' in job && job?.deleted}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4" role="alert">
<p class="font-bold">Deleted</p>
<p>The content of this run was deleted (by an admin, no less)</p>
</div>
{/if}
<!-- Arguments and actions -->
<div class="flex flex-col mr-2 sm:mr-0 sm:grid sm:grid-cols-3 sm:gap-5">
<div class="col-span-2">
<JobArgs args={job?.args} />
{#if job?.job_kind == 'flow' || job?.job_kind == 'flowpreview'}
<div class="mt-10" />
<FlowProgressBar {job} class="py-4" />
<div class="max-w-lg">
<FlowStatusViewer
jobId={job.id}
on:jobsLoaded={({ detail }) => {
job = detail
}}
/>
</div>
{/if}
</div>
<div>
<Skeleton loading={!job} layout={[[9.5]]} />
{#if job}<FlowMetadata {job} />{/if}
</div>
</div>
{/if}
</CenteredPage>
{#if job?.job_kind !== 'flow' && job?.job_kind !== 'flowpreview'}
<!-- Logs and outputs-->
<div class="mr-2 sm:mr-0 mt-12">
<Tabs bind:selected={viewTab}>
<Tab value="result">Result</Tab>
<Tab value="logs">Logs</Tab>
{#if job?.job_kind == 'dependencies'}
<Tab value="code">Dependencies</Tab>
{:else if job?.job_kind == 'preview'}
<Tab value="code">Code</Tab>
{/if}
</Tabs>
<Skeleton loading={!job} layout={[[5]]} />
{#if job}
<div class="flex flex-row border rounded-md p-3 max-h-1/2 overflow-auto">
{#if viewTab == 'logs'}
<div class="w-full">
<LogViewer isLoading={!(job && 'logs' in job && job.logs)} content={job?.logs} />
</div>
{:else if viewTab == 'code'}
{#if job && 'raw_code' in job && job.raw_code}
<HighlightCode language={job.language} code={job.raw_code} />
{:else if job}
No code is available
{:else}
<Skeleton layout={[[5]]} />
{/if}
{:else if job !== undefined && 'result' in job && job.result !== undefined}
<DisplayResult result={job.result} />
{:else if job}
No output is available yet
{/if}
</div>
{/if}
</div>
{/if}
</CenteredPage>
{/if}
@@ -59,8 +59,8 @@
requestBody: args,
scheduledFor
})
sendUserToast(`Job <a href='/run/${run}'>${run}</a> started`)
goto('/run/' + run)
sendUserToast(`Job <a href='/run/${run}?workspace=${$workspaceStore}'>${run}</a> started`)
goto('/run/' + run + '?workspace=' + $workspaceStore)
} catch (err) {
sendUserToast(`Could not create job: ${err}`, true)
}