feat: timelines for apps

This commit is contained in:
Ruben Fiszel
2023-10-20 22:50:36 +02:00
parent 3065acab1b
commit c00ff69f04
14 changed files with 191 additions and 54 deletions
+8
View File
@@ -4651,6 +4651,7 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
- $ref: "#/components/parameters/GetStarted"
responses:
"200":
description: result
@@ -4662,6 +4663,8 @@ paths:
completed:
type: boolean
result: {}
started:
type: boolean
required:
- completed
- result
@@ -6405,6 +6408,11 @@ components:
required: true
schema:
type: string
GetStarted:
name: get_started
in: query
schema:
type: boolean
schemas:
$ref: "../../openflow.openapi.yaml#/components/schemas"
+31 -3
View File
@@ -2758,26 +2758,54 @@ async fn get_completed_job_result(
#[derive(Serialize)]
struct CompletedJobResult<'c> {
started: Option<bool>,
completed: bool,
result: Option<&'c JsonRawValue>,
}
#[derive(Deserialize)]
struct GetCompletedJobQuery {
get_started: Option<bool>,
}
async fn get_completed_job_result_maybe(
Extension(db): Extension<DB>,
Path((w_id, id)): Path<(String, Uuid)>,
Query(GetCompletedJobQuery { get_started }): Query<GetCompletedJobQuery>,
) -> error::Result<Response> {
let result_o =
sqlx::query("SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(w_id)
.bind(&w_id)
.fetch_optional(&db)
.await?;
if let Some(result) = result_o {
let res = RawResult::from_row(&result)?;
Ok(Json(CompletedJobResult { completed: true, result: Some(res.result) }).into_response())
Ok(Json(CompletedJobResult {
started: Some(true),
completed: true,
result: Some(res.result),
})
.into_response())
} else if get_started.is_some_and(|x| x) {
let started = sqlx::query_scalar!(
"SELECT running FROM queue WHERE id = $1 AND workspace_id = $2",
id,
w_id
)
.fetch_optional(&db)
.await?
.unwrap_or(false);
Ok(
Json(CompletedJobResult { started: Some(started), completed: false, result: None })
.into_response(),
)
} else {
Ok(Json(CompletedJobResult { completed: false, result: None }).into_response())
Ok(
Json(CompletedJobResult { started: None, completed: false, result: None })
.into_response(),
)
}
}
@@ -121,14 +121,16 @@
>{#if min && total}
<div class="flex flex-col gap-2 w-full">
{#each items?.[k] ?? [] as b}
<TimelineBar
id={b?.id}
{total}
{min}
started_at={b.started_at}
len={b?.started_at ? b?.duration_ms ?? now - b?.started_at : 0}
running={b?.duration_ms == undefined}
/>
<div class="flex w-full">
<TimelineBar
id={b?.id}
{total}
{min}
started_at={b.started_at}
len={b?.started_at ? b?.duration_ms ?? now - b?.started_at : 0}
running={b?.duration_ms == undefined}
/>
</div>
{/each}
</div>
{/if}</div
@@ -10,6 +10,7 @@
export let job: { completed: boolean; result: any; id: string } | undefined = undefined
export let workspaceOverride: string | undefined = undefined
export let notfound = false
export let isEditor = false
const dispatch = createEventDispatcher()
@@ -26,8 +27,10 @@
$: isLoading = currentId !== undefined
let running = false
export async function abstractRun(fn: () => Promise<string>) {
try {
running = false
isLoading = true
clearCurrentJob()
const startedAt = Date.now()
@@ -106,6 +109,7 @@
export async function cancelJob() {
const id = currentId
if (id) {
dispatch('cancel', id)
currentId = undefined
try {
await JobService.cancelQueuedJob({
@@ -121,6 +125,7 @@
export async function clearCurrentJob() {
if (currentId) {
dispatch('cancel', currentId)
job = undefined
await cancelJob()
}
@@ -145,8 +150,13 @@
try {
let maybe_job = await JobService.getCompletedJobResultMaybe({
workspace: workspace ?? '',
id
id,
getStarted: isEditor
})
if (maybe_job.started && !running) {
running = true
dispatch('running', id)
}
if (maybe_job.completed) {
isCompleted = true
if (currentId === id) {
+20 -14
View File
@@ -9,25 +9,31 @@
export let len: number
export let id: string
export let running: boolean
export let concat: boolean = false
export let gray: boolean = false
</script>
{#if min && started_at}
<div class="flex w-full">
{#if min && started_at != undefined}
{#if !concat}
<div style="width: {((started_at - min) / total) * 100}%" class="h-4" />
<Popover
style="width: {(len / total) * 100}%"
class="h-4 {running
? 'bg-blue-400/90'
: 'bg-blue-500/90'} rounded-sm center-center text-white text-2xs whitespace-nowrap hover:outline outline-1 outline-black"
{/if}
<Popover
style="width: {(len / total) * 100}%"
class="h-4 {gray
? 'bg-gray-500'
: running
? 'bg-blue-400/90'
: 'bg-blue-500/90'} rounded-sm center-center text-white text-2xs whitespace-nowrap hover:outline outline-1 outline-black"
>
<svelte:fragment slot="text"
><a href="/run/{id}" class="inline-flex items-center gap-1" target="_blank"
>{id} <ExternalLink size={14} /></a
></svelte:fragment
>
<svelte:fragment slot="text"
><a href="/run/{id}" class="inline-flex items-center gap-1" target="_blank"
>{id} <ExternalLink size={14} /></a
></svelte:fragment
>
{#if len > 0}
<span class={len / total < 0.09 ? '-ml-14 text-primary' : ''}
>{#if len}{msToSec(len, 1)}s{/if}</span
>
</Popover>
</div>
{/if}
</Popover>
{/if}
@@ -344,14 +344,15 @@
...oldJob,
...(result ? { result } : {}),
...(transformer ? { transformer } : {}),
duration_ms: oldJob?.started_at ? Date.now() - oldJob?.started_at : 1
error,
duration_ms: oldJob?.started_compute_at ? Date.now() - oldJob?.started_compute_at : 1
}
$jobsById[jobId] = job
}
if (error) {
$errorByComponent[id] = { id, error }
$errorByComponent[id] = { id: jobId, error }
}
}
@@ -449,9 +450,9 @@
let cancellableRun: ((inlineScript?: InlineScript) => CancelablePromise<void>) | undefined =
undefined
let didInitialRun = false
let didInitialRun = $initialized.initialized
onMount(() => {
didInitialRun = false
didInitialRun = $initialized.initialized
cancellableRun = (inlineScript?: InlineScript) => {
let rejectCb: (err: Error) => void
let p: Partial<CancelablePromise<void>> = new Promise<void>((resolve, reject) => {
@@ -523,7 +524,9 @@
{/if}
<ResultJobLoader
{isEditor}
on:started={(e) => {
console.log('started', e.detail)
loading = true
setJobId(e.detail)
dispatch('started', e.detail)
@@ -537,8 +540,19 @@
on:cancel={(e) => {
let jobId = e.detail
let job = $jobsById[jobId]
if (job && job.started_at) {
$jobsById[jobId] = { ...job, duration_ms: Date.now() - job.started_at }
if (job && job.started_at && !job.duration_ms) {
$jobsById[jobId] = {
...job,
duration_ms: Date.now() - (job.started_compute_at ?? job.started_at)
}
}
}}
on:running={(e) => {
console.log('running', e.detail)
let jobId = e.detail
let job = $jobsById[jobId]
if (job && !job.started_compute_at) {
$jobsById[jobId] = { ...job, started_compute_at: Date.now() }
}
}}
on:doneError={(e) => {
@@ -146,6 +146,7 @@
let scale = writable(100)
setContext<AppEditorContext>('AppEditorContext', {
refreshComponents: writable(undefined),
history,
pickVariableCallback,
movingcomponents: writable(undefined),
@@ -37,6 +37,7 @@
Laptop2,
Loader2,
MoreVertical,
RefreshCw,
Smartphone
} from 'lucide-svelte'
import { getContext } from 'svelte'
@@ -106,7 +107,8 @@
openDebugRun
} = getContext<AppViewerContext>('AppViewerContext')
const { history, jobsDrawerOpen } = getContext<AppEditorContext>('AppEditorContext')
const { history, jobsDrawerOpen, refreshComponents } =
getContext<AppEditorContext>('AppEditorContext')
const loading = {
publish: false,
@@ -657,9 +659,12 @@
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-surface-secondary hover:text-blue-400',
selectedJob.error ? 'border border-red-500 text-primary' : '',
selectedJob.error && $errorByComponent[selectedJob.component]?.id == id
? 'bg-red-400'
: '',
selectedJobId == id ? 'bg-surface-secondary text-blue-600' : ''
? selectedJobId == id
? 'bg-red-600 !border-blue-600'
: 'bg-red-400'
: selectedJobId == id
? 'text-blue-600'
: ''
)}
on:click={() => {
selectedJobId = id
@@ -827,6 +832,16 @@
</Pane>
</Splitpanes>
<svelte:fragment slot="actions">
<Button
on:click={() => {
$refreshComponents?.()
}}
size="xs"
title="Refresh App"
>
Refresh App&nbsp;<RefreshCw size={16} />
</Button>
<Button
size="md"
color="light"
@@ -14,7 +14,10 @@
let debounced = debounce(() => computeItems($jobs), 30)
$: $jobs && $jobsById && debounced()
let items: Record<string, { started_at?: number; duration_ms?: number; id: string }[]> = {}
let items: Record<
string,
{ started_at?: number; started_compute_at?: number; duration_ms?: number; id: string }[]
> = {}
export function reset() {
min = undefined
@@ -27,7 +30,10 @@
let isStillRunning = false
let nitems: Record<string, { started_at?: number; duration_ms?: number; id: string }[]> = {}
let nitems: Record<
string,
{ started_at?: number; started_compute_at?: number; duration_ms?: number; id: string }[]
> = {}
jobs.forEach((k) => {
let v = $jobsById[k]
if (v.started_at) {
@@ -41,8 +47,8 @@
isStillRunning = true
}
if (!isStillRunning) {
if (v.started_at && v.duration_ms) {
let lmax = v.started_at + v.duration_ms
if (v.started_compute_at && v.duration_ms) {
let lmax = v.started_compute_at + v.duration_ms
if (!nmax) {
nmax = lmax
} else {
@@ -53,7 +59,12 @@
if (!nitems[v.component]) {
nitems[v.component] = []
}
nitems[v.component].push({ started_at: v.started_at, duration_ms: v.duration_ms, id: v.job })
nitems[v.component].push({
started_at: v.started_at,
duration_ms: v.duration_ms,
started_compute_at: v.started_compute_at,
id: v.job
})
})
Object.values(nitems).forEach((v) => {
@@ -96,6 +107,19 @@
{JSON.stringify(items, null, 4)}
</pre> -->
<div class="divide-y">
<div class="flex flex-row-reverse mb-2 items-center text-sm text-secondary px-2">
<div class="flex gap-4 items-center">
<div class="flex gap-2 items-center">
<div>Waiting for executor</div>
<div class="h-4 w-4 bg-gray-500" />
</div>
<div class="flex gap-2 items-center">
<div>Execution</div>
<div class="h-4 w-4 bg-blue-500/90" />
</div>
</div>
</div>
{#each Object.entries(items) as [k, v]}
<div class="px-2 py-2 grid grid-cols-12 w-full"
><div class="col-span-2">{k}</div>
@@ -103,14 +127,33 @@
>{#if min && total}
<div class="flex flex-col gap-2 w-full">
{#each v ?? [] as b}
<TimelineBar
id={b?.id}
{total}
{min}
started_at={b.started_at}
len={b?.started_at ? b?.duration_ms ?? now - b?.started_at : 0}
running={b?.duration_ms == undefined}
/>
{@const waitingLen = b?.started_at
? b.started_compute_at
? b.started_compute_at - b?.started_at
: now - b?.started_at
: 0}
<div class="flex w-full">
<TimelineBar
id={b?.id}
{total}
{min}
gray
started_at={b.started_at}
len={waitingLen < 100 ? 0 : waitingLen}
running={b?.started_compute_at == undefined}
/>
{#if b.started_compute_at}
<TimelineBar
id={b?.id}
{total}
{min}
concat
started_at={b.started_compute_at}
len={b.started_compute_at ? b?.duration_ms ?? now - b?.started_compute_at : 0}
running={b?.duration_ms == undefined}
/>
{/if}
</div>
{/each}
</div>
{/if}</div
@@ -2,13 +2,15 @@
import { RefreshCw } from 'lucide-svelte'
import { getContext, onMount } from 'svelte'
import Button from '../../common/button/Button.svelte'
import type { AppViewerContext } from '../types'
import type { AppEditorContext, AppViewerContext } from '../types'
import { allItems } from '../utils'
import ButtonDropdown from '$lib/components/common/button/ButtonDropdown.svelte'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
import { classNames } from '$lib/utils'
const { runnableComponents, app, initialized } = getContext<AppViewerContext>('AppViewerContext')
const appEditorContext = getContext<AppEditorContext>('AppEditorContext')
let loading: boolean = false
let timeout: NodeJS.Timer | undefined = undefined
let interval: number | undefined = undefined
@@ -17,10 +19,16 @@
$: !firstLoad &&
$initialized.initializedComponents?.length ==
allItems($app.grid, $app.subgrids).length + $app.hiddenInlineScripts.length &&
allItems($app.grid, $app.subgrids).length + ($app.hiddenInlineScripts?.length ?? 0) &&
refresh()
$: componentNumber = Object.values($runnableComponents).filter((x) => x.autoRefresh).length
onMount(() => {
if (appEditorContext) {
appEditorContext.refreshComponents.set(refresh)
}
})
function onClick(stopAfterClear = true) {
if (timeout) {
clearInterval(timeout)
@@ -7,7 +7,7 @@
const { app } = getContext<AppViewerContext>('AppViewerContext')
</script>
{#each $app.hiddenInlineScripts as action, index}
{#each $app.hiddenInlineScripts ?? [] as action, index}
{#if !action.hidden}
<BackgroundScriptOutput id={BG_PREFIX + index} name={action.name} first={index === 0} />
{/if}
@@ -77,7 +77,7 @@
renameComponent(from, to, item.data)
})
$app.hiddenInlineScripts.forEach((x) => {
$app.hiddenInlineScripts?.forEach((x) => {
processRunnable(from, to, x)
})
}
@@ -213,6 +213,7 @@ export type AppViewerContext = {
error?: any
transformer?: { result?: string; error?: string }
started_at?: number
started_compute_at?: number
duration_ms?: number
}>>,
noBackend: boolean
@@ -249,6 +250,7 @@ export type AppViewerContext = {
}
export type AppEditorContext = {
refreshComponents: Writable<(() => void) | undefined>
history: History<App> | undefined
pickVariableCallback: Writable<((path: string) => void) | undefined>
selectedComponentInEditor: Writable<string | undefined>
@@ -60,7 +60,7 @@ export function isFlowTainted(flow: Flow) {
}
export function isAppTainted(app: App) {
return !(app.grid.length === 0 && app.hiddenInlineScripts.length === 0)
return !(app.grid.length === 0 && app.hiddenInlineScripts?.length === 0)
}
export function updateFlowModuleById(