mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat(frontend): Add runs preview popup (#1405)
* feat(frontend): Add runs preview popup * fix(frontend): Remove events from closed drawer * remove comment * limit open previews to one * handle running jobs * update styling * position correctly --------- Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
export let result: any
|
||||
export let requireHtmlApproval = false
|
||||
export let filename: string | undefined = undefined
|
||||
export let disableExpand = false
|
||||
|
||||
let resultKind:
|
||||
| 'json'
|
||||
@@ -101,10 +102,12 @@
|
||||
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
|
||||
class="mb-2 w-full text-sm text-gray-700 relative"
|
||||
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
|
||||
<div class="text-gray-500 text-xs absolute top-5 right-0">
|
||||
<button on:click={jsonViewer.openDrawer}>Expand</button>
|
||||
</div></div
|
||||
>{/if}{#if !forceJson && resultKind == 'table-col'}<div
|
||||
{#if !disableExpand}
|
||||
<div class="text-gray-500 text-xs absolute top-5 right-0">
|
||||
<button on:click={jsonViewer.openDrawer}>Expand</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>{/if}{#if !forceJson && resultKind == 'table-col'}<div
|
||||
class="grid grid-flow-col-dense border border-gray-200 rounded-md"
|
||||
>
|
||||
{#each Object.keys(result) as col}
|
||||
@@ -245,19 +248,21 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Portal>
|
||||
<Drawer bind:this={jsonViewer} size="900px">
|
||||
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
on:click={() => copyToClipboard(JSON.stringify(result, null, 4))}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Portal>
|
||||
{#if !disableExpand}
|
||||
<Portal>
|
||||
<Drawer bind:this={jsonViewer} size="900px">
|
||||
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
on:click={() => copyToClipboard(JSON.stringify(result, null, 4))}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Portal>
|
||||
{/if}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
|
||||
export let args: any
|
||||
export let tableClass = ''
|
||||
</script>
|
||||
|
||||
<TableCustom class="py-2">
|
||||
<TableCustom class="py-2 {tableClass}">
|
||||
<tr slot="header-row">
|
||||
<th>Argument</th>
|
||||
<th>Value</th>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let isLoading: boolean
|
||||
export let duration: number | undefined = undefined
|
||||
export let mem: number | undefined = undefined
|
||||
export let wrapperClass = ''
|
||||
|
||||
let scroll = true
|
||||
let div: HTMLElement | null = null
|
||||
@@ -33,7 +34,7 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="relative w-full h-full">
|
||||
<div class="relative w-full h-full {wrapperClass}">
|
||||
<div bind:this={div} class="w-full h-full overflow-auto bg-gray-50 relative">
|
||||
<div
|
||||
class="sticky top-0 right-0 w-full flex flex-row-reverse justify-between text-gray-500 text-sm bg-gray-50/20"
|
||||
|
||||
@@ -198,3 +198,19 @@
|
||||
currentId = undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<slot
|
||||
{job}
|
||||
{isLoading}
|
||||
{workspaceOverride}
|
||||
{notfound}
|
||||
{abstractRun}
|
||||
{runScriptByPath}
|
||||
{runFlowByPath}
|
||||
{runPreview}
|
||||
{cancelJob}
|
||||
{clearCurrentJob}
|
||||
{watchJob}
|
||||
{loadTestJob}
|
||||
{syncer}
|
||||
/>
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
z-index: -1;
|
||||
transition: z-index var(--duration) step-end;
|
||||
overflow: clip;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drawer.open {
|
||||
@@ -107,6 +108,7 @@
|
||||
width: 100%;
|
||||
z-index: 1002;
|
||||
transition: z-index var(--duration) step-start;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import { check } from 'svelte-awesome/icons'
|
||||
import { Badge } from '../common'
|
||||
import ScheduleEditor from '../ScheduleEditor.svelte'
|
||||
import JobPreview from './JobPreview.svelte'
|
||||
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
@@ -65,182 +66,187 @@
|
||||
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
|
||||
<div class="border border-gray-400 rounded py-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 w-full gap-4">
|
||||
<div class="flex-col col-span-2">
|
||||
<div class="flex flex-row text-sm">
|
||||
{#if job === undefined}
|
||||
No job found
|
||||
{:else}
|
||||
<div class="block text-center align-middle px-6">
|
||||
{#if 'success' in job && job.success}
|
||||
{#if job.is_skipped}
|
||||
<JobPreview {job}>
|
||||
<div class="border border-gray-400 rounded py-4 h-[72px]">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 w-full gap-4">
|
||||
<div class="flex-col col-span-2">
|
||||
<div class="flex flex-row text-sm">
|
||||
{#if job === undefined}
|
||||
No job found
|
||||
{:else}
|
||||
<div class="block text-center align-middle px-6">
|
||||
{#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 'success' in job}
|
||||
<Icon
|
||||
class="text-green-600"
|
||||
data={faFastForward}
|
||||
class="text-red-700"
|
||||
data={faTimes}
|
||||
scale={SMALL_ICON_SCALE}
|
||||
label="Job completed successfully but was skipped"
|
||||
label="Job completed with an error"
|
||||
/>
|
||||
{:else if 'running' in job && job.running}
|
||||
<Icon
|
||||
class="text-yellow-500"
|
||||
data={faCircle}
|
||||
scale={SMALL_ICON_SCALE}
|
||||
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={SMALL_ICON_SCALE}
|
||||
label="Job is scheduled to run at a later time"
|
||||
/>
|
||||
{:else}
|
||||
<Icon
|
||||
class="text-green-600"
|
||||
data={check}
|
||||
class="text-gray-500"
|
||||
data={faHourglassHalf}
|
||||
scale={SMALL_ICON_SCALE}
|
||||
label="Job completed successfully"
|
||||
label="Job is waiting for an executor"
|
||||
/>
|
||||
{/if}
|
||||
{:else if 'success' in job}
|
||||
<Icon
|
||||
class="text-red-700"
|
||||
data={faTimes}
|
||||
scale={SMALL_ICON_SCALE}
|
||||
label="Job completed with an error"
|
||||
/>
|
||||
{:else if 'running' in job && job.running}
|
||||
<Icon
|
||||
class="text-yellow-500"
|
||||
data={faCircle}
|
||||
scale={SMALL_ICON_SCALE}
|
||||
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={SMALL_ICON_SCALE}
|
||||
label="Job is scheduled to run at a later time"
|
||||
/>
|
||||
{:else}
|
||||
<Icon
|
||||
class="text-gray-500"
|
||||
data={faHourglassHalf}
|
||||
scale={SMALL_ICON_SCALE}
|
||||
label="Job is waiting for an executor"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div class="whitespace-nowrap">
|
||||
{#if job.script_path}
|
||||
<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}?workspace={job.workspace_id}">Preview without path </a>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}"
|
||||
>lock deps of {truncateHash(job.script_hash ?? '')}</a
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div class="whitespace-nowrap">
|
||||
{#if job.script_path}
|
||||
<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}?workspace={job.workspace_id}">Preview without path </a>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
|
||||
<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}?workspace={job.workspace_id}">no op</a>
|
||||
{/if}
|
||||
<button
|
||||
class="ml-1"
|
||||
on:click={() => {
|
||||
goto(`/runs/${job.script_path}?${$page.url.searchParams.toString()}`)
|
||||
}}><Badge><Icon scale={0.7} data={faSearch} /></Badge></button
|
||||
>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'identity'}
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}">no op</a>
|
||||
{/if}
|
||||
<button
|
||||
class="ml-1"
|
||||
on:click={() => {
|
||||
goto(`/runs/${job.script_path}?${$page.url.searchParams.toString()}`)
|
||||
}}><Badge><Icon scale={0.7} data={faSearch} /></Badge></button
|
||||
>
|
||||
</div>
|
||||
<div class="whitespace-nowrap">
|
||||
{#if 'job_kind' in job}<a href="/run/{job.id}"
|
||||
><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}?workspace={job.workspace_id}"
|
||||
><Badge color="gray">flow step</Badge></a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="whitespace-nowrap">
|
||||
{#if 'job_kind' in job}<a href="/run/{job.id}"
|
||||
><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}?workspace={job.workspace_id}"
|
||||
><Badge color="gray">flow step</Badge></a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pl-14 italic text-gray-500 text-2xs whitespace-nowrap overflow-hidden"
|
||||
>{truncateRev(job.id, 8, '')}</div
|
||||
>
|
||||
</div>
|
||||
<div class="bg-white grid grid-cols-2 gap-x-2 col-span-2">
|
||||
<div class="w-full text-gray-500 text-xs text-left flex flex-col gap-1 mx-4 overflow-hidden">
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faUser} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
By {job.created_by}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faHourglassHalf} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
Ran in {msToSec(job.duration_ms)}s</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="pl-14 italic text-gray-500 text-2xs whitespace-nowrap overflow-hidden"
|
||||
>{truncateRev(job.id, 8, '')}</div
|
||||
>
|
||||
</div>
|
||||
<div class="text-gray-500 text-xs text-left place-self-start flex flex-col gap-1">
|
||||
{#if 'started_at' in job && job.started_at}
|
||||
<div class="bg-white grid grid-cols-2 gap-x-2 col-span-2">
|
||||
<div
|
||||
class="w-full text-gray-500 text-xs text-left flex flex-col gap-1 mx-4 overflow-hidden"
|
||||
>
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-1.5"
|
||||
<Icon class="text-gray-700" data={faUser} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
By {job.created_by}</span
|
||||
>
|
||||
<span>
|
||||
{#if job?.['duration_ms']}
|
||||
Ended {#key time}
|
||||
{endedDate(job.started_at, job?.['duration_ms'])}{/key}
|
||||
{:else}
|
||||
Started {#key time}
|
||||
{displayDaysAgo(job.started_at ?? '')}{/key}
|
||||
{/if}</span
|
||||
>
|
||||
</span></div
|
||||
>
|
||||
{/if}
|
||||
{#if 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="inline-flex gap-1">
|
||||
<CalendarClock size={13} class="-ml-0.5" />
|
||||
<span>
|
||||
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1 ">Scheduled</span>
|
||||
for {displayDate(job.scheduled_for ?? '')}
|
||||
</span>
|
||||
</div>
|
||||
{:else if 'scheduled_for' in job && !job.running}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1 "
|
||||
>Waiting for an executor</span
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faHourglassHalf} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if job && job.parent_job}
|
||||
{#if job.is_flow_step}
|
||||
<Icon class="text-gray-700" data={faBarsStaggered} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-1"
|
||||
Ran in {msToSec(job.duration_ms)}s</span
|
||||
>
|
||||
Step of flow <a href={`/run/${job.parent_job}`}>{truncateRev(job.parent_job, 6)}</a
|
||||
></span
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-500 text-xs text-left place-self-start flex flex-col gap-1">
|
||||
{#if 'started_at' in job && job.started_at}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-1.5"
|
||||
>
|
||||
{:else}
|
||||
<Icon class="text-gray-700" data={faRobot} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-1"
|
||||
>
|
||||
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-1"
|
||||
>Schedule <button
|
||||
class="break-words text-blue-400 font-normal truncate text-xs"
|
||||
on:click={() =>
|
||||
scheduleEditor?.openEdit(job.schedule_path ?? '', job.job_kind == 'flow')}
|
||||
>{job.schedule_path}</button
|
||||
></span
|
||||
<span>
|
||||
{#if job?.['duration_ms']}
|
||||
Ended {#key time}
|
||||
{endedDate(job.started_at, job?.['duration_ms'])}{/key}
|
||||
{:else}
|
||||
Started {#key time}
|
||||
{displayDaysAgo(job.started_at ?? '')}{/key}
|
||||
{/if}</span
|
||||
>
|
||||
</span></div
|
||||
>
|
||||
{/if}
|
||||
{#if 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="inline-flex gap-1">
|
||||
<CalendarClock size={13} class="-ml-0.5" />
|
||||
<span>
|
||||
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1">Scheduled</span>
|
||||
for {displayDate(job.scheduled_for ?? '')}
|
||||
</span>
|
||||
</div>
|
||||
{:else if 'scheduled_for' in job && !job.running}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1"
|
||||
>Waiting for an executor</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if job && job.parent_job}
|
||||
{#if job.is_flow_step}
|
||||
<Icon class="text-gray-700" data={faBarsStaggered} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-1"
|
||||
>
|
||||
Step of flow <a href={`/run/${job.parent_job}`}
|
||||
>{truncateRev(job.parent_job, 6)}</a
|
||||
></span
|
||||
>
|
||||
{:else}
|
||||
<Icon class="text-gray-700" data={faRobot} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-1"
|
||||
>
|
||||
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-1"
|
||||
>Schedule <button
|
||||
class="break-words text-blue-400 font-normal truncate text-xs"
|
||||
on:click={() =>
|
||||
scheduleEditor?.openEdit(job.schedule_path ?? '', job.job_kind == 'flow')}
|
||||
>{job.schedule_path}</button
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</JobPreview>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<script context="module" lang="ts">
|
||||
const openStore = writable('')
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Job } from '../../gen'
|
||||
import TestJobLoader from '../TestJobLoader.svelte'
|
||||
import DisplayResult from '../DisplayResult.svelte'
|
||||
import JobArgs from '../JobArgs.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import LogViewer from '../LogViewer.svelte'
|
||||
|
||||
const POPUP_HEIGHT = 240 as const
|
||||
|
||||
export let job: Job | undefined
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
let watchJob: (id: string) => Promise<void>
|
||||
let args = job?.args
|
||||
let result: any
|
||||
let loaded = false
|
||||
let wrapper: HTMLElement
|
||||
let popupOnTop = true
|
||||
|
||||
$: open = $openStore === job?.id
|
||||
$: completed = job?.type === Job.type.COMPLETED_JOB
|
||||
$: logs = job?.logs || logs
|
||||
|
||||
async function instantOpen() {
|
||||
if (!job) {
|
||||
return
|
||||
}
|
||||
popupOnTop = wrapper.getBoundingClientRect().top > POPUP_HEIGHT
|
||||
openStore.set(job.id)
|
||||
if (!loaded) {
|
||||
await tick()
|
||||
watchJob && watchJob(job.id)
|
||||
}
|
||||
}
|
||||
|
||||
function staggeredOpen() {
|
||||
timeout = setTimeout(
|
||||
async () => {
|
||||
timeout = undefined
|
||||
await instantOpen()
|
||||
},
|
||||
loaded ? 100 : 300
|
||||
)
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = undefined
|
||||
}
|
||||
if (open) {
|
||||
openStore.set('')
|
||||
}
|
||||
}
|
||||
|
||||
function onDone(event: { detail: Job }) {
|
||||
job = event.detail
|
||||
result = job['result']
|
||||
loaded = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={({ key }) => ['Escape', 'Esc'].includes(key) && close()} />
|
||||
<TestJobLoader bind:job bind:watchJob on:done={onDone} />
|
||||
|
||||
<div
|
||||
on:mouseenter={staggeredOpen}
|
||||
on:mouseleave={close}
|
||||
on:focusin={instantOpen}
|
||||
on:focusout={close}
|
||||
bind:this={wrapper}
|
||||
class="relative"
|
||||
>
|
||||
<slot {open} />
|
||||
{#if open}
|
||||
<div
|
||||
transition:fade={{ duration: 100 }}
|
||||
class="absolute z-50 {popupOnTop ? 'bottom-[65px]' : 'top-[65px]'} left-4 bg-white rounded
|
||||
border border-gray-300 shadow-xl flex justify-start items-start w-[600px] max-w-full h-60
|
||||
overflow-hidden"
|
||||
>
|
||||
<div class="w-1/2 h-full overflow-auto px-2">
|
||||
<JobArgs {args} tableClass="!pt-0 !min-w-0 !block" />
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-auto p-2">
|
||||
{#if completed}
|
||||
<DisplayResult {result} disableExpand />
|
||||
{:else}
|
||||
<div class="text-sm font-semibold text-gray-600 mb-1"> Job is still running </div>
|
||||
<LogViewer content={logs} isLoading wrapperClass="!h-[calc(100%-24px)]" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user