mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
refactor logs panel everywhere applicable (#625)
* init * init * progress * progress * refactor logs entirely * fix sqlx
This commit is contained in:
@@ -3163,6 +3163,29 @@
|
||||
},
|
||||
"query": "\n SELECT SUM(duration_ms)\n FROM completed_job\n WHERE permissioned_as = $1\n AND created_at > NOW() - INTERVAL '1200 seconds'\n AND workspace_id = $2"
|
||||
},
|
||||
"e7051b21ffd80de0cb0644a71c89c9f63abed6b446d1e232adbb6931efad6d6d": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE id = $3 AND workspace_id = $4 RETURNING id"
|
||||
},
|
||||
"ee99cb974fb35d2d92fa96cecb53b8ad3a729ac710f362dc55a576613b66f9be": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
+1
-8
@@ -778,7 +778,7 @@ async fn cancel_job(
|
||||
|
||||
let job_option = sqlx::query_scalar!(
|
||||
"UPDATE queue SET canceled = true, canceled_by = $1, canceled_reason = $2 WHERE id = $3 \
|
||||
AND schedule_path IS NULL AND workspace_id = $4 RETURNING id",
|
||||
AND workspace_id = $4 RETURNING id",
|
||||
&authed.username,
|
||||
reason,
|
||||
id,
|
||||
@@ -808,13 +808,6 @@ async fn cancel_job(
|
||||
"queued job id {} exists but is already completed and cannot be canceled",
|
||||
id
|
||||
)),
|
||||
Some(Job::QueuedJob(job)) if job.schedule_path.is_some() => {
|
||||
error::Error::BadRequest(format!(
|
||||
"queued job id {} exists but has been created by a scheduler
|
||||
and can only be only canceled by disabling the parent scheduler",
|
||||
id
|
||||
))
|
||||
}
|
||||
_ => error::Error::NotFound(format!("queued job id {} does not exist", id)),
|
||||
};
|
||||
Err(err)
|
||||
|
||||
@@ -52,74 +52,80 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if result}
|
||||
{#if typeof result == 'object' && Object.keys(result).length > 0}<div>
|
||||
The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
|
||||
</div>
|
||||
{/if}
|
||||
{#if resultKind == 'table-col'}
|
||||
<div class="grid grid-flow-col-dense border border-gray-200 rounded-md ">
|
||||
{#each Object.keys(result) as col}
|
||||
<div class="flex flex-col max-h-40 min-w-full overflow-auto">
|
||||
<div class="px-12 text-left uppercase border-b bg-gray-50 overflow-hidden rounded-t-md ">
|
||||
{col}
|
||||
</div>
|
||||
{#if Array.isArray(result[col])}
|
||||
{#each result[col] as item}
|
||||
<div class="px-12 text-left whitespace-nowrap">
|
||||
{typeof item === 'string' ? item : JSON.stringify(item)}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if resultKind == 'table-row'}<div
|
||||
class="grid grid-flow-col-dense border border-gray-200 rounded-md "
|
||||
>
|
||||
<TableCustom>
|
||||
<tbody slot="body">
|
||||
{#each asListOfList(result) as row}
|
||||
<tr>
|
||||
{#each row as v}
|
||||
<td>{truncate(JSON.stringify(v), 200) ?? ''}</td>
|
||||
<div>
|
||||
{#if result}
|
||||
{#if typeof result == 'object' && Object.keys(result).length > 0}
|
||||
<div>
|
||||
The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
|
||||
</div>{/if}{#if resultKind == 'table-col'}
|
||||
<div class="grid grid-flow-col-dense border border-gray-200 rounded-md ">
|
||||
{#each Object.keys(result) as col}
|
||||
<div class="flex flex-col max-h-40 min-w-full overflow-auto">
|
||||
<div
|
||||
class="px-12 text-left uppercase border-b bg-gray-50 overflow-hidden rounded-t-md "
|
||||
>
|
||||
{col}
|
||||
</div>
|
||||
{#if Array.isArray(result[col])}
|
||||
{#each result[col] as item}
|
||||
<div class="px-12 text-left whitespace-nowrap">
|
||||
{typeof item === 'string' ? item : JSON.stringify(item)}
|
||||
</div>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
{:else if resultKind == 'png'}
|
||||
<div class="h-full">
|
||||
Result is an image: <img
|
||||
alt="png rendered"
|
||||
class="w-auto h-full"
|
||||
src="data:image/png;base64,{result.png}"
|
||||
/>
|
||||
</div>
|
||||
{:else if resultKind == 'jpeg'}
|
||||
<div class="h-full">
|
||||
Result is an image: <img
|
||||
alt="jpeg rendered"
|
||||
class="w-auto h-full"
|
||||
src="data:image/jpeg;base64,{result.jpeg}"
|
||||
/>
|
||||
</div>
|
||||
{:else if resultKind == 'gif'}
|
||||
<div class="h-full">
|
||||
Result is an image: <img
|
||||
alt="gif rendered"
|
||||
class="w-auto h-full"
|
||||
src="data:image/gif;base64,{result.gif}"
|
||||
/>
|
||||
</div>
|
||||
{:else if resultKind == 'file'}
|
||||
<div>
|
||||
Result is a file: <a
|
||||
download="windmill.file"
|
||||
href="data:application/octet-stream;base64,{result.file}">Download</a
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if resultKind == 'table-row'}<div
|
||||
class="grid grid-flow-col-dense border border-gray-200 rounded-md "
|
||||
>
|
||||
</div>
|
||||
{:else}<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
|
||||
<TableCustom>
|
||||
<tbody slot="body">
|
||||
{#each asListOfList(result) as row}
|
||||
<tr>
|
||||
{#each row as v}
|
||||
<td>{truncate(JSON.stringify(v), 200) ?? ''}</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
{:else if resultKind == 'png'}
|
||||
<div class="h-full">
|
||||
Result is an image: <img
|
||||
alt="png rendered"
|
||||
class="w-auto h-full"
|
||||
src="data:image/png;base64,{result.png}"
|
||||
/>
|
||||
</div>
|
||||
{:else if resultKind == 'jpeg'}
|
||||
<div class="h-full">
|
||||
Result is an image: <img
|
||||
alt="jpeg rendered"
|
||||
class="w-auto h-full"
|
||||
src="data:image/jpeg;base64,{result.jpeg}"
|
||||
/>
|
||||
</div>
|
||||
{:else if resultKind == 'gif'}
|
||||
<div class="h-full">
|
||||
Result is an image: <img
|
||||
alt="gif rendered"
|
||||
class="w-auto h-full"
|
||||
src="data:image/gif;base64,{result.gif}"
|
||||
/>
|
||||
</div>
|
||||
{:else if resultKind == 'file'}
|
||||
<div>
|
||||
Result is a file: <a
|
||||
download="windmill.file"
|
||||
href="data:application/octet-stream;base64,{result.file}">Download</a
|
||||
>
|
||||
</div>
|
||||
{:else}<Highlight
|
||||
language={json}
|
||||
code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { Flow } from '$lib/gen'
|
||||
import { sendUserToast, truncateRev } from '$lib/utils'
|
||||
import { HSplitPane } from 'svelte-split-pane'
|
||||
|
||||
import { mapJobResultsToFlowState } from './flows/flowStateUtils'
|
||||
import { flowStore } from './flows/flowStore'
|
||||
import { runFlowPreview } from './flows/utils'
|
||||
import FlowStatusViewer from './FlowStatusViewer.svelte'
|
||||
import RunForm from './RunForm.svelte'
|
||||
|
||||
export let indexes: string
|
||||
export let schema: Schema
|
||||
|
||||
const [i, j] = indexes.split('-').filter(Boolean).map(Number)
|
||||
|
||||
let stepArgs: Record<string, any> = {}
|
||||
let jobId: string
|
||||
|
||||
export async function runPreview(args: any) {
|
||||
let newFlow: Flow = setInputTransformFromArgs(extractStep($flowStore), args)
|
||||
jobId = await runFlowPreview(args, newFlow)
|
||||
|
||||
sendUserToast(`started preview ${truncateRev(jobId, 10)}`)
|
||||
}
|
||||
|
||||
function extractStep(flow: Flow): Flow {
|
||||
const localFlow = JSON.parse(JSON.stringify(flow))
|
||||
const mod = flow.value.modules[i].value
|
||||
if (j != undefined && mod.type === 'forloopflow') {
|
||||
localFlow.value.modules = mod.modules.slice(j, j + 1)
|
||||
} else {
|
||||
localFlow.value.modules = flow.value.modules.slice(i, i + 1)
|
||||
}
|
||||
localFlow.schema = schema
|
||||
return localFlow
|
||||
}
|
||||
|
||||
function setInputTransformFromArgs(flow: Flow, args: any) {
|
||||
let input_transforms = {}
|
||||
Object.entries(args).forEach(([key, value]) => {
|
||||
input_transforms[key] = {
|
||||
type: 'static',
|
||||
value: value
|
||||
}
|
||||
})
|
||||
flow.value.modules[0].input_transforms = input_transforms
|
||||
return flow
|
||||
}
|
||||
</script>
|
||||
|
||||
<HSplitPane leftPaneSize="50%" rightPaneSize="50%" minLeftPaneSize="20%" minRightPaneSize="20%">
|
||||
<left slot="left" class="relative">
|
||||
<div class="overflow-auto h-full p-4">
|
||||
<RunForm
|
||||
runnable={extractStep($flowStore)}
|
||||
runAction={(_, args) => runPreview(args)}
|
||||
schedulable={false}
|
||||
buttonText="Test just this step"
|
||||
detailed={false}
|
||||
args={stepArgs}
|
||||
/>
|
||||
</div>
|
||||
</left>
|
||||
<right slot="right">
|
||||
<div class="overflow-auto h-full p-4">
|
||||
{#if jobId}
|
||||
<FlowStatusViewer
|
||||
{jobId}
|
||||
on:jobsLoaded={(e) => mapJobResultsToFlowState(e.detail, 'justthis', i, j)}
|
||||
/>
|
||||
{:else}
|
||||
<span class="text-sm">No results yet</span>
|
||||
{/if}
|
||||
</div>
|
||||
</right>
|
||||
</HSplitPane>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
export let content: string | undefined
|
||||
export let isLoading: boolean
|
||||
|
||||
let scroll = true
|
||||
let div: HTMLElement | null = null
|
||||
|
||||
$: if (content != undefined) {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
$: content && scrollToBottom()
|
||||
|
||||
export function scrollToBottom() {
|
||||
scroll && setTimeout(() => div?.scroll({ top: div?.scrollHeight, behavior: 'smooth' }), 100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative w-full h-full">
|
||||
<div bind:this={div} class="w-full h-full overflow-auto bg-gray-50 relative">
|
||||
<pre
|
||||
class="whitespace-pre-wrap break-all bg-gray-50 text-xs w-full p-2">{#if content}{content}{:else if isLoading}Waiting for job to start...{:else}No logs is available yet{/if}</pre>
|
||||
</div>
|
||||
<div class="absolute top-0 right-0 mr-2 text-gray-500 text-sm bg-gray-50/20">
|
||||
Auto scroll
|
||||
<input type="checkbox" bind:checked={scroll} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { FlowModule, Job } from '$lib/gen'
|
||||
import { getScriptByPath, sendUserToast, truncateRev } from '$lib/utils'
|
||||
|
||||
import { HSplitPane, VSplitPane } from 'svelte-split-pane'
|
||||
|
||||
import RunForm from './RunForm.svelte'
|
||||
import TestJobLoader from './TestJobLoader.svelte'
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import { mapJobResultsToFlowState } from './flows/flowStateUtils'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { faRotateRight } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
let testJobLoader: TestJobLoader
|
||||
|
||||
// Test
|
||||
let testIsLoading = false
|
||||
let testJob: Job | undefined
|
||||
|
||||
export let mod: FlowModule
|
||||
export let schema: Schema
|
||||
|
||||
let stepArgs: Record<string, any> = {}
|
||||
|
||||
export async function runTest(args: any) {
|
||||
const val = mod.value
|
||||
if (val.type == 'rawscript') {
|
||||
await testJobLoader?.runPreview(val.path, val.content, val.language, args)
|
||||
} else if (val.type == 'script') {
|
||||
const script = await getScriptByPath(val.path)
|
||||
await testJobLoader?.runPreview(val.path, script.content, script.language, args)
|
||||
} else {
|
||||
throw Error('not testable module type')
|
||||
}
|
||||
sendUserToast(`started test ${truncateRev(testJob?.id ?? '', 10)}`)
|
||||
}
|
||||
|
||||
function jobDone() {
|
||||
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob') {
|
||||
//mapJobResultsToFlowState(testJob.result, 'justthis', 0, 0)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
on:done={jobDone}
|
||||
bind:this={testJobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
bind:job={testJob}
|
||||
/>
|
||||
<HSplitPane leftPaneSize="50%" rightPaneSize="50%" minLeftPaneSize="20%" minRightPaneSize="20%">
|
||||
<left slot="left" class="relative">
|
||||
<div class="overflow-auto h-full p-4">
|
||||
<RunForm
|
||||
runnable={{ summary: mod.summary ?? '', schema, description: '' }}
|
||||
runAction={(_, args) => runTest(args)}
|
||||
schedulable={false}
|
||||
buttonText="Test just this step"
|
||||
detailed={false}
|
||||
args={stepArgs}
|
||||
/>
|
||||
{#if testIsLoading}
|
||||
<Button
|
||||
on:click={testJobLoader?.cancelJob}
|
||||
btnClasses="w-full"
|
||||
color="red"
|
||||
size="xs"
|
||||
startIcon={{
|
||||
icon: faRotateRight,
|
||||
classes: 'animate-spin'
|
||||
}}
|
||||
>
|
||||
'Cancel'
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</left>
|
||||
<right slot="right">
|
||||
<div class="overflow-auto h-full">
|
||||
<VSplitPane topPanelSize="50%" downPanelSize="50%">
|
||||
<top slot="top">
|
||||
<LogViewer content={testJob?.logs} isLoading={testIsLoading} />
|
||||
</top>
|
||||
<down slot="down">
|
||||
<pre
|
||||
class="overflow-x-auto break-all relative h-full p-2 text-sm">{#if testJob && 'result' in testJob && testJob.result}<DisplayResult
|
||||
result={testJob.result}
|
||||
/>
|
||||
{:else if testIsLoading}Waiting for Result...
|
||||
{:else}Test to see result here
|
||||
{/if}
|
||||
</pre>
|
||||
</down>
|
||||
</VSplitPane>
|
||||
</div>
|
||||
</right>
|
||||
</HSplitPane>
|
||||
@@ -9,8 +9,9 @@
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
export let runnable: Script | Flow | undefined
|
||||
export let runnable: { summary: string; schema: Schema; description: string }
|
||||
export let runAction: (scheduledForStr: string | undefined, args: Record<string, any>) => void
|
||||
export let buttonText = 'Run'
|
||||
export let schedulable = true
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { CompletedJob, Job, JobService } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { classNames, emptySchema, scriptLangToEditorLang } from '$lib/utils'
|
||||
import { emptySchema, scriptLangToEditorLang } from '$lib/utils'
|
||||
import {
|
||||
faCheck,
|
||||
faExclamationTriangle,
|
||||
faPlay,
|
||||
faRotateRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import Editor from './Editor.svelte'
|
||||
|
||||
@@ -23,6 +22,7 @@
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import EditorBar from './EditorBar.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import TestJobLoader from './TestJobLoader.svelte'
|
||||
|
||||
// Exported
|
||||
export let schema: Schema = emptySchema()
|
||||
@@ -34,22 +34,19 @@
|
||||
|
||||
// Internal state
|
||||
let editor: Editor
|
||||
let logPanel: LogPanel
|
||||
|
||||
// Preview args input
|
||||
let testJobLoader: TestJobLoader
|
||||
|
||||
// Test args input
|
||||
let args: Record<string, any> = {}
|
||||
let isValid: boolean = true
|
||||
|
||||
// Preview
|
||||
let previewIsLoading = false
|
||||
let previewIntervalId: NodeJS.Timer
|
||||
let previewJob: Job | undefined
|
||||
// Test
|
||||
let testIsLoading = false
|
||||
let testJob: Job | undefined
|
||||
let pastPreviews: CompletedJob[] = []
|
||||
let lastSave: string | null
|
||||
|
||||
let syncIteration: number = 0
|
||||
let ITERATIONS_BEFORE_SLOW_REFRESH = 100
|
||||
|
||||
$: lastSave = localStorage.getItem(path ?? 'last_save')
|
||||
|
||||
export function getEditor(): Editor {
|
||||
@@ -59,50 +56,15 @@
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
|
||||
event.preventDefault()
|
||||
runPreview()
|
||||
runTest()
|
||||
}
|
||||
}
|
||||
|
||||
let div: HTMLElement | null = null
|
||||
|
||||
export async function runPreview(): Promise<void> {
|
||||
try {
|
||||
if (previewIntervalId) {
|
||||
clearInterval(previewIntervalId)
|
||||
}
|
||||
if (previewIsLoading && previewJob) {
|
||||
JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: previewJob.id,
|
||||
requestBody: {}
|
||||
})
|
||||
}
|
||||
previewIsLoading = true
|
||||
|
||||
const previewId = await JobService.runScriptPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
content: editor.getCode(),
|
||||
args: args,
|
||||
language: lang
|
||||
}
|
||||
})
|
||||
previewJob = undefined
|
||||
loadPreviewJob(previewId)
|
||||
syncIteration = 0
|
||||
previewIntervalId = setInterval(() => {
|
||||
syncer(previewId)
|
||||
}, 500)
|
||||
logPanel?.setFocusToLogs()
|
||||
//TODO fetch preview, every x time, until it's completed
|
||||
} catch (err) {
|
||||
previewIsLoading = false
|
||||
throw err
|
||||
}
|
||||
function runTest() {
|
||||
testJobLoader.runPreview(path, editor.getCode(), lang, args)
|
||||
}
|
||||
|
||||
async function loadPastPreviews(): Promise<void> {
|
||||
async function loadPastTests(): Promise<void> {
|
||||
pastPreviews = await JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore!,
|
||||
jobKinds: 'preview',
|
||||
@@ -111,37 +73,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
async function loadPreviewJob(id: string): Promise<void> {
|
||||
try {
|
||||
if (previewJob && `running` in previewJob) {
|
||||
let previewJobUpdates = await JobService.getJobUpdates({
|
||||
workspace: $workspaceStore!,
|
||||
id,
|
||||
running: previewJob.running,
|
||||
logOffset: previewJob.logs?.length ?? 0
|
||||
})
|
||||
|
||||
if (previewJobUpdates.new_logs) {
|
||||
previewJob.logs = (previewJob.logs ?? '').concat(previewJobUpdates.new_logs)
|
||||
}
|
||||
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
|
||||
previewJob = await JobService.getJob({ workspace: $workspaceStore!, id })
|
||||
}
|
||||
} else {
|
||||
previewJob = await JobService.getJob({ workspace: $workspaceStore!, id })
|
||||
}
|
||||
if (previewJob?.type === 'CompletedJob') {
|
||||
//only CompletedJob has success property
|
||||
clearInterval(previewIntervalId)
|
||||
previewIsLoading = false
|
||||
loadPastPreviews()
|
||||
}
|
||||
div?.scroll({ top: div?.scrollHeight, behavior: 'smooth' })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function inferSchema() {
|
||||
let isDefault: string[] = []
|
||||
Object.entries(args).forEach(([k, v]) => {
|
||||
@@ -161,45 +92,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncer(id: string): void {
|
||||
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
|
||||
loadPreviewJob(id)
|
||||
if (previewIntervalId) {
|
||||
clearInterval(previewIntervalId)
|
||||
previewIntervalId = setInterval(() => loadPreviewJob(id), 5000)
|
||||
}
|
||||
} else {
|
||||
syncIteration++
|
||||
loadPreviewJob(id)
|
||||
}
|
||||
}
|
||||
|
||||
let syncCode: NodeJS.Timer
|
||||
|
||||
onMount(() => {
|
||||
inferSchema()
|
||||
syncCode = setInterval(() => {
|
||||
const newCode = editor?.getCode()
|
||||
if (newCode && code != newCode) {
|
||||
code = editor.getCode()
|
||||
}
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) {
|
||||
code = editor.getCode()
|
||||
}
|
||||
if (previewIntervalId) {
|
||||
clearInterval(previewIntervalId)
|
||||
}
|
||||
if (syncCode) {
|
||||
clearInterval(syncCode)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
on:done={loadPastTests}
|
||||
bind:this={testJobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
bind:job={testJob}
|
||||
/>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<div class="border-b shadow-sm p-1 pr-4">
|
||||
@@ -221,21 +122,6 @@
|
||||
Sync from Github
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
on:click={() => runPreview()}
|
||||
disabled={previewIsLoading}
|
||||
btnClasses="w-50 ml-1"
|
||||
size="xs"
|
||||
startIcon={{
|
||||
icon: previewIsLoading ? faRotateRight : faPlay,
|
||||
classes: classNames(previewIsLoading ? 'animate-spin' : 'animate-none')
|
||||
}}
|
||||
>
|
||||
{previewIsLoading ? 'Running' : 'Run preview (Ctrl+Enter)'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,11 +137,11 @@
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
{code}
|
||||
bind:code
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
cmdEnterAction={() => {
|
||||
runPreview()
|
||||
runTest()
|
||||
}}
|
||||
formatAction={async () => {
|
||||
code = getEditor().getCode()
|
||||
@@ -306,14 +192,41 @@
|
||||
</div>
|
||||
</top>
|
||||
<down slot="down">
|
||||
<div class="pt-1 h-full overflow-auto">
|
||||
<div class="h-full overflow-hidden">
|
||||
<div class="px-2 py-1">
|
||||
{#if testIsLoading}
|
||||
<Button
|
||||
on:click={testJobLoader?.cancelJob}
|
||||
btnClasses="w-full"
|
||||
color="red"
|
||||
size="xs"
|
||||
startIcon={{
|
||||
icon: faRotateRight,
|
||||
classes: 'animate-spin'
|
||||
}}
|
||||
>
|
||||
'Cancel'
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
on:click={runTest}
|
||||
btnClasses="w-full"
|
||||
size="xs"
|
||||
startIcon={{
|
||||
icon: faPlay,
|
||||
classes: 'animate-none'
|
||||
}}
|
||||
>
|
||||
{testIsLoading ? 'Running' : 'Test (Ctrl+Enter)'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<LogPanel
|
||||
bind:this={logPanel}
|
||||
{path}
|
||||
{lang}
|
||||
{previewJob}
|
||||
previewJob={testJob}
|
||||
{pastPreviews}
|
||||
{previewIsLoading}
|
||||
previewIsLoading={testIsLoading}
|
||||
bind:lastSave
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { Job, JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
import type { Preview } from '$lib/gen/models/Preview'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let isLoading = false
|
||||
export let job: Job | undefined = undefined
|
||||
|
||||
let intervalId: NodeJS.Timer
|
||||
|
||||
let syncIteration: number = 0
|
||||
let ITERATIONS_BEFORE_SLOW_REFRESH = 100
|
||||
|
||||
export async function runPreview(
|
||||
path: string | undefined,
|
||||
code: string,
|
||||
lang: 'deno' | 'go' | 'python3',
|
||||
args: Record<string, any>
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
if (isLoading && job) {
|
||||
JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: job.id,
|
||||
requestBody: {}
|
||||
})
|
||||
}
|
||||
isLoading = true
|
||||
|
||||
const testId = await JobService.runScriptPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
content: code,
|
||||
args,
|
||||
language: lang as Preview.language
|
||||
}
|
||||
})
|
||||
await watchJob(testId)
|
||||
} catch (err) {
|
||||
isLoading = false
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelJob() {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: job?.id ?? '',
|
||||
requestBody: {}
|
||||
})
|
||||
console.log('cancelled')
|
||||
}
|
||||
|
||||
export async function watchJob(testId: string) {
|
||||
console.log('watch jobs')
|
||||
|
||||
job = undefined
|
||||
await loadTestJob(testId)
|
||||
syncIteration = 0
|
||||
intervalId = setInterval(() => {
|
||||
syncer(testId)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function loadTestJob(id: string): Promise<void> {
|
||||
try {
|
||||
if (job && `running` in job) {
|
||||
let previewJobUpdates = await JobService.getJobUpdates({
|
||||
workspace: $workspaceStore!,
|
||||
id,
|
||||
running: job.running,
|
||||
logOffset: job.logs?.length ?? 0
|
||||
})
|
||||
|
||||
if (previewJobUpdates.new_logs) {
|
||||
job.logs = (job.logs ?? '').concat(previewJobUpdates.new_logs)
|
||||
}
|
||||
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
|
||||
job = await JobService.getJob({ workspace: $workspaceStore!, id })
|
||||
}
|
||||
} else {
|
||||
job = await JobService.getJob({ workspace: $workspaceStore!, id })
|
||||
}
|
||||
if (job?.type === 'CompletedJob') {
|
||||
//only CompletedJob has success property
|
||||
dispatch('done', job)
|
||||
clearInterval(intervalId)
|
||||
isLoading = false
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
function syncer(id: string): void {
|
||||
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
|
||||
loadTestJob(id)
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = setInterval(() => loadTestJob(id), 2000)
|
||||
}
|
||||
} else {
|
||||
syncIteration++
|
||||
loadTestJob(id)
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
export let size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' = 'md'
|
||||
export let color: 'blue' | 'dark' | 'light' = 'blue'
|
||||
export let color: 'blue' | 'dark' | 'light' | 'red' = 'blue'
|
||||
export let variant: 'contained' | 'border' = 'contained'
|
||||
export let btnClasses: string = ''
|
||||
export let disabled: boolean = false
|
||||
@@ -17,6 +17,8 @@
|
||||
switch (color) {
|
||||
case 'blue':
|
||||
return 'bg-blue-500 hover:bg-blue-700 focus:ring-blue-300 text-white '
|
||||
case 'red':
|
||||
return 'bg-red-500 hover:bg-red-700 focus:ring-red-300 text-white '
|
||||
case 'light':
|
||||
return 'text-gray-800 bg-white hover:bg-gray-100 focus:ring-gray-300'
|
||||
default:
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let selected: string
|
||||
|
||||
@@ -15,6 +18,8 @@
|
||||
|
||||
const selectedContent = writable(selected)
|
||||
|
||||
$: $selectedContent && dispatch('selected', $selectedContent)
|
||||
|
||||
setContext<TabsContext>('Tabs', {
|
||||
selected: selectedContent,
|
||||
update: (value: string) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Editor from '$lib/components/Editor.svelte'
|
||||
import EditorBar from '$lib/components/EditorBar.svelte'
|
||||
import FlowPreview from '$lib/components/FlowPreview.svelte'
|
||||
import ModulePreview from '$lib/components/ModulePreview.svelte'
|
||||
import FlowInputs from './FlowInputs.svelte'
|
||||
import {
|
||||
createInlineScriptModule,
|
||||
@@ -156,7 +156,7 @@
|
||||
</PropPickerWrapper>
|
||||
</TabContent>
|
||||
<TabContent value="test" class="flex flex-col flex-1 h-full">
|
||||
<FlowPreview indexes={$selectedId} schema={flowModuleState.schema} />
|
||||
<ModulePreview mod={flowModule} schema={flowModuleState.schema} />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="advanced" class="flex flex-col flex-1 h-full">
|
||||
|
||||
@@ -44,18 +44,6 @@ export function getTypeAsString(arg: any): string {
|
||||
return typeof arg
|
||||
}
|
||||
|
||||
export function scrollIntoView(element: any) {
|
||||
if (!element) return
|
||||
|
||||
var headerOffset = 45
|
||||
var elementPosition = element.getBoundingClientRect().top
|
||||
var offsetPosition = elementPosition + window.pageYOffset - headerOffset
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadSchemaFromModule(module: FlowModule): Promise<{
|
||||
input_transforms: Record<string, InputTransform>
|
||||
@@ -122,9 +110,8 @@ export function getDefaultExpr(
|
||||
previousExpr?: string
|
||||
) {
|
||||
const expr = previousExpr ?? `previous_result.${key}`
|
||||
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill${
|
||||
importPath ? `@${importPath}` : ''
|
||||
}'
|
||||
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill${importPath ? `@${importPath}` : ''
|
||||
}'
|
||||
|
||||
${expr}`
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
import HighlightCode from '../HighlightCode.svelte'
|
||||
import { VSplitPane } from 'svelte-split-pane'
|
||||
import LogViewer from '../LogViewer.svelte'
|
||||
|
||||
export let path: string | undefined
|
||||
export let lang: Preview.language
|
||||
@@ -69,23 +70,18 @@
|
||||
<Tab value="last_save"><span class="text-xs">Last save</span></Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="logs" class="h-full w-full">
|
||||
<TabContent value="logs" class="h-full w-full relative">
|
||||
<VSplitPane topPanelSize="50%" downPanelSize="50%">
|
||||
<top slot="top">
|
||||
<pre
|
||||
class="whitespace-pre-wrap break-all relative h-full bg-gray-50 text-xs w-full h-full p-2">{#if previewJob && previewJob.logs}{previewJob.logs}
|
||||
{:else if previewIsLoading}Running...
|
||||
{:else}No preview is available yet
|
||||
{/if}
|
||||
</pre>
|
||||
<LogViewer content={previewJob?.logs} isLoading={previewIsLoading} />
|
||||
</top>
|
||||
<down slot="down">
|
||||
<pre
|
||||
class="overflow-x-auto break-all relative h-full p-2 text-sm">{#if previewJob && 'result' in previewJob && previewJob.result}<DisplayResult
|
||||
result={previewJob.result}
|
||||
/>
|
||||
{:else if previewIsLoading}Running...
|
||||
{:else}No result is available yet
|
||||
{:else if previewIsLoading}Waiting for Result...
|
||||
{:else}Test to see result here
|
||||
{/if}
|
||||
</pre>
|
||||
</down>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { JobService, Job } from '$lib/gen'
|
||||
import { onDestroy } from 'svelte'
|
||||
import {
|
||||
canWrite,
|
||||
displayDaysAgo,
|
||||
@@ -47,23 +46,27 @@
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import ArgInfo from '$lib/components/ArgInfo.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
import LogViewer from '$lib/components/LogViewer.svelte'
|
||||
|
||||
let workspace_id_query: string | undefined = $page.url.searchParams.get('workspace') ?? undefined
|
||||
let workspace_id: string | undefined
|
||||
|
||||
let intervalId: NodeJS.Timer
|
||||
let job: Job | undefined
|
||||
let error: Error | undefined
|
||||
const iconScale = 1
|
||||
let syncIteration: number = 0
|
||||
const iterationsBeforeSlowRefresh = 100
|
||||
|
||||
let viewTab: 'result' | 'logs' | 'code' = 'result'
|
||||
|
||||
// Test
|
||||
let testIsLoading = false
|
||||
|
||||
let testJobLoader: TestJobLoader
|
||||
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
async function deleteCompletedJob(id: string): Promise<void> {
|
||||
await JobService.deleteCompletedJob({ workspace: workspace_id!, id })
|
||||
loadLogs()
|
||||
getLogs()
|
||||
}
|
||||
|
||||
async function cancelJob(id: string) {
|
||||
@@ -75,54 +78,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCompleted(): Promise<void> {
|
||||
if (job?.type === 'CompletedJob') {
|
||||
//only CompletedJob has success property
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs(): Promise<void> {
|
||||
try {
|
||||
let jobId = $page.params.run
|
||||
if (job && `running` in job && job.job_kind != 'flow' && job.job_kind != 'flowpreview') {
|
||||
let jobUpdates = await JobService.getJobUpdates({
|
||||
workspace: workspace_id!,
|
||||
id: jobId,
|
||||
running: job.running,
|
||||
logOffset: job.logs?.length ?? 0
|
||||
})
|
||||
if (jobUpdates.new_logs) {
|
||||
job.logs = (job.logs ?? '').concat(jobUpdates.new_logs)
|
||||
}
|
||||
if ((jobUpdates.running ?? false) || (jobUpdates.completed ?? false)) {
|
||||
job = await JobService.getJob({ workspace: workspace_id!, id: jobId })
|
||||
}
|
||||
} else {
|
||||
job = await JobService.getJob({ workspace: workspace_id!, id: jobId })
|
||||
}
|
||||
|
||||
checkCompleted()
|
||||
initView()
|
||||
} catch (err) {
|
||||
error = err
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
function syncer(): void {
|
||||
if (syncIteration > iterationsBeforeSlowRefresh) {
|
||||
loadLogs()
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = setInterval(loadLogs, 5000)
|
||||
}
|
||||
} else {
|
||||
syncIteration++
|
||||
loadLogs()
|
||||
}
|
||||
}
|
||||
|
||||
// If we get results, focus on that tab. Else, focus on logs
|
||||
function initView(): void {
|
||||
if (job && 'result' in job && job.result) {
|
||||
@@ -132,19 +87,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && $page.params.run) {
|
||||
workspace_id = workspace_id_query ?? $workspaceStore
|
||||
intervalId && clearInterval(intervalId)
|
||||
intervalId = setInterval(syncer, 500)
|
||||
}
|
||||
async function getLogs() {
|
||||
await testJobLoader?.watchJob($page.params.run)
|
||||
initView()
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
intervalId && clearInterval(intervalId)
|
||||
})
|
||||
$: {
|
||||
if ($workspaceStore && $page.params.run && testJobLoader) {
|
||||
workspace_id = workspace_id_query ?? $workspaceStore
|
||||
getLogs()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
on:done={() => (viewTab = 'result')}
|
||||
bind:this={testJobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
bind:job
|
||||
/>
|
||||
|
||||
<CenteredPage>
|
||||
<div class="flex flex-row justify-between">
|
||||
<h1>
|
||||
@@ -246,7 +208,7 @@
|
||||
{#if job?.job_kind == 'script'}
|
||||
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
|
||||
<a
|
||||
href="/scripts/edit/{job?.script_hash}"
|
||||
href="/scripts/edit/{job?.script_hash}?step=2"
|
||||
class="default-button-secondary py-1 text-sm px-2"
|
||||
><Icon data={faEdit} scale={0.6} /><span class="px-1">Edit</span></a
|
||||
>
|
||||
@@ -430,21 +392,20 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row border rounded-md p-3">
|
||||
<pre
|
||||
class="text-xs overflow-auto max-h-96 w-full p-3">{#if viewTab == 'logs'}{#if job && 'logs' in job && job.logs}{job.logs}
|
||||
{:else if job}No logs are available yet
|
||||
{:else}Loading...{/if}
|
||||
{: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}Loading...{/if}
|
||||
{:else if job && 'result' in job && job.result}<DisplayResult result={job.result} />
|
||||
{:else if job}No output is available yet
|
||||
{:else}Loading...
|
||||
{/if}
|
||||
</pre>
|
||||
<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}Loading...{/if}
|
||||
{:else if job && 'result' in job && job.result}<DisplayResult result={job.result} />
|
||||
{:else if job}No output is available yet
|
||||
{:else}Loading...
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user