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:
Ádám Kovács
2023-04-15 10:02:04 +02:00
committed by GitHub
parent a85ad82b3a
commit 0dbb5a58f2
7 changed files with 308 additions and 176 deletions
@@ -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}
+2 -1
View File
@@ -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>
+2 -1
View File
@@ -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 {
+160 -154
View File
@@ -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>