refactor logs panel everywhere applicable (#625)

* init

* init

* progress

* progress

* refactor logs entirely

* fix sqlx
This commit is contained in:
Ruben Fiszel
2022-09-25 18:03:51 +02:00
committed by GitHub
parent ba203b8674
commit 4044edf8d6
15 changed files with 459 additions and 400 deletions
+23
View File
@@ -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
View File
@@ -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>
+2 -1
View File
@@ -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
+52 -139
View File
@@ -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">
+2 -15
View File
@@ -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>
+40 -79
View File
@@ -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}