Files
windmill/frontend/src/lib/components/scriptEditor/LogPanel.svelte
T
pyranotaandRuben Fiszel d3e0b1c636 feat: Allow setProgress and getProgress from within the script (#4400)
* Allow setting progress explicitly from script body.

This feature exposes:
 * `getProgress`
 * `setProgress`
 * `incProgress`

API in TypeScript client (python is coming soon).

NOTE: Progress cannot be out of range 0..100 and cannot decrease.

With exposed APIs there is also UI changes, so progress can be shown for individual jobs as well.

For optimization reasons, jobs start to ask for progress only after N-seconds of execution.

* feat: Add `shell.nix`

If you dont have anything but nix, dont worry, run nix-shell in root, or activate with direnv and get all needed dependencies

NOTE: You will still need docker

* feat: Add `dev.nu` to typescript client

Little helper function, allowing developer to work on ts client easier.

To use:
  `./dev.nu watch`

Now add import of windmill in body of your script and `//nobundle` on top of the file

Edit ts client in your favourite editor and hit save. Script will do the rest.

* Cleanup files

* Fix: Failed to deserialize query string: missing field `get_progress`

* perf: Implement non-naive polling mechanism for getting job progress

* Add independant delay for getProgress

Problem in `TestJobLoader`:

There should be 2 delays:

    One until we find our first progress (every 5s)
    Once we found our first progress, we can do it every second

* nit: Use `query_scalar!` instead of `query_as`

* Fix: Sql error, no rows returned by a query that expected to return at least one row

* refactor: Remove global CSS for JobProgressBar

* Change UI for progress of flow subjobs

* Replace `Step 1` with `Running` in ProgressBar for individual jobs

* Remove `incProgress`

incProgress is not very usefull and error-prone

* perf: Set metric only for jobs that are actually using it

(https://github.com/windmill-labs/windmill/pull/4373#discussion_r1759843773)

* Offload registering progress from clients to server

* Add `jobId?` argument to typescript-client's `setProgress` and `getProgress`

Allows to set progress of other jobs and flows,
if jobId specified, than flow id will be inferred automatically.

Could be used by SDK.

* Add `Error::MetricNotFound` for better error handling

* Fix: Make `JobProgressBar` display in red when failed

* Add persistant progress bar

Now you can reload the page after job is done and progress will be still there

* Allow succeeded individual job's progress bar stick to 100%

* Add python support 

* nit: Remove usage of undefined variable in python-client

* Add `async` in ts client (for error handling)

* nit(frontend): Remove unused import

* Dont load JobProgressBar when it is not needed

* nit: npm check fix

* cargo sqlx prepare

* fix sqlx

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2024-09-18 01:16:19 +02:00

257 lines
7.7 KiB
Svelte

<script lang="ts">
import {
type CompletedJob,
type Job,
JobService,
OpenAPI,
type Preview,
type WorkflowStatus
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import { displayDate } from '$lib/utils'
import Tabs from '../common/tabs/Tabs.svelte'
import Tab from '../common/tabs/Tab.svelte'
import DisplayResult from '../DisplayResult.svelte'
import Drawer from '../common/drawer/Drawer.svelte'
import DrawerContent from '../common/drawer/DrawerContent.svelte'
import HighlightCode from '../HighlightCode.svelte'
import LogViewer from '../LogViewer.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import SplitPanesWrapper from '../splitPanes/SplitPanesWrapper.svelte'
import { CheckCircle2, Loader2, XCircle } from 'lucide-svelte'
import type Editor from '../Editor.svelte'
import type DiffEditor from '../DiffEditor.svelte'
import ScriptFix from '../copilot/ScriptFix.svelte'
import Cell from '../table/Cell.svelte'
import DataTable from '../table/DataTable.svelte'
import Head from '../table/Head.svelte'
import WorkflowTimeline from '../WorkflowTimeline.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
export let lang: Preview['language'] | undefined
export let previewIsLoading = false
export let previewJob: Job | undefined
export let pastPreviews: CompletedJob[] = []
export let editor: Editor | undefined = undefined
export let diffEditor: DiffEditor | undefined = undefined
export let args: Record<string, any> | undefined = undefined
export let workspace: string | undefined = undefined
type DrawerContent = {
mode: 'json' | Preview['language'] | 'plain'
title: string
content: any
}
let selectedTab = 'logs'
let drawerOpen: boolean = false
let drawerContent: DrawerContent | undefined = undefined
export function setFocusToLogs() {
selectedTab = 'logs'
}
function openDrawer(newContent: DrawerContent) {
drawerContent = newContent
drawerOpen = true
}
function closeDrawer() {
drawerOpen = false
}
function asWorkflowStatus(x: any): Record<string, WorkflowStatus> {
return x as Record<string, WorkflowStatus>
}
let forceJson = false
</script>
<Drawer bind:open={drawerOpen} size="800px">
<DrawerContent title={drawerContent?.title} on:close={() => closeDrawer()}>
{#if drawerContent?.mode === 'json'}
<DisplayResult
workspaceId={previewJob?.workspace_id}
jobId={previewJob?.id}
result={drawerContent.content}
/>
{:else if drawerContent?.mode === 'plain'}
<pre
class="overflow-x-auto break-words relative h-full m-2 text-xs bg-surface shadow-inner p-2"
>{drawerContent?.content}
</pre>
{:else if drawerContent?.mode}
<HighlightCode language={drawerContent?.mode} code={drawerContent?.content} />
{/if}
</DrawerContent>
</Drawer>
<Tabs bind:selected={selectedTab} class="mt-1">
<Tab value="logs" size="xs">Logs & Result</Tab>
<Tab value="history" size="xs">History</Tab>
<svelte:fragment slot="content">
<!--
TabContent would wrap it in an extra div which is undesirable in this situation
because SplitPanesWrapper uses the parent element as a reference point.
-->
{#if selectedTab === 'logs'}
<SplitPanesWrapper>
<Splitpanes horizontal>
{#if previewJob?.is_flow_step == false && previewJob?.flow_status && !(typeof previewJob.flow_status == 'object' && '_metadata' in previewJob.flow_status)}
<Pane class="relative">
<WorkflowTimeline
flow_status={asWorkflowStatus(previewJob.flow_status)}
flowDone={previewJob.type == 'CompletedJob'}
/>
</Pane>
{/if}
<Pane class="relative">
<LogViewer
jobId={previewJob?.id}
duration={previewJob?.['duration_ms']}
mem={previewJob?.['mem_peak']}
content={previewJob?.logs}
isLoading={previewJob?.['running'] == false && previewIsLoading}
tag={previewJob?.tag}
/>
</Pane>
<Pane>
<slot></slot>
{#if previewJob != undefined && 'result' in previewJob}
<div class="relative w-full h-full p-2">
<div class="relative">
<DisplayResult
bind:forceJson
workspaceId={previewJob?.workspace_id}
jobId={previewJob?.id}
result={previewJob.result}
>
<svelte:fragment slot="copilot-fix">
{#if lang && editor && diffEditor && args && previewJob?.result && typeof previewJob?.result == 'object' && `error` in previewJob?.result && previewJob?.result.error}
<ScriptFix
error={JSON.stringify(previewJob.result.error)}
{lang}
{editor}
{diffEditor}
{args}
/>
{/if}
</svelte:fragment>
</DisplayResult>
</div>
</div>
{:else}
<div class="text-sm text-tertiary p-2 flex justify-between items-center">
<span>
{#if previewIsLoading}
<Loader2 class="animate-spin" />
{:else}
Test to see the result here
{/if}
</span>
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering"
>
The result renderer in Windmill supports rich display rendering, allowing you to
customize the display format of your results.
</Tooltip>
</div>
{/if}
</Pane>
</Splitpanes>
</SplitPanesWrapper>
{/if}
{#if selectedTab === 'history'}
<div>
<DataTable size="xs" noBorder>
<Head>
<tr>
<Cell first>Id</Cell>
<Cell>Created at</Cell>
<Cell>Success</Cell>
<Cell>Result</Cell>
<Cell>Code</Cell>
<Cell last>Logs</Cell>
</tr>
</Head>
<tbody class="divide-y">
{#each pastPreviews as { id, created_at, success }}
<tr>
<Cell first>
<a
class="pr-3"
href="{base}/run/{id}?workspace={workspace ?? $workspaceStore}"
target="_blank">{id.substring(30)}</a
>
</Cell>
<Cell>{displayDate(created_at)}</Cell>
<Cell>
{#if success}
<CheckCircle2 size={10} class="text-green-600" />
{:else}
<XCircle size={10} class="text-red-700" />
{/if}
</Cell>
<Cell>
<button
class="text-xs"
on:click|preventDefault={() => {
openDrawer({ mode: 'json', content: undefined, title: 'Result' })
JobService.getCompletedJobResult({
workspace: workspace ?? $workspaceStore ?? 'NO_W',
id
}).then((res) => {
drawerContent && (drawerContent.content = res)
})
}}
>
See Result
</button>
</Cell>
<Cell>
<button
class="text-xs"
on:click|preventDefault={async () => {
const code = (
await JobService.getCompletedJob({
workspace: workspace ?? $workspaceStore ?? 'NO_W',
id
})
).raw_code
openDrawer({
mode: lang ?? 'plain',
content: String(code),
title: `Code ${lang}`
})
}}
>
View code
</button>
</Cell>
<Cell last>
<button
class="text-xs"
on:click|preventDefault={async () => {
const logs = await (
await fetch(
OpenAPI.BASE + `/w/${workspace ?? $workspaceStore}/jobs_u/get_logs/${id}`
)
).text()
console.log(logs)
openDrawer({ mode: 'plain', content: String(logs), title: `Logs for ${id}` })
}}
>
View logs
</button>
</Cell>
</tr>
{/each}
</tbody>
</DataTable>
</div>
{/if}
</svelte:fragment>
</Tabs>