mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 16:03:27 +00:00
improve devex for local flow (#2543)
* all * all * improve dev pages * improve dev pages * fix multiple bugs * fix multiple bugs
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
// windmill
|
||||
export { setClient } from "https://deno.land/x/windmill@v1.188.1/mod.ts";
|
||||
export * from "https://deno.land/x/windmill@v1.188.1/windmill-api/index.ts";
|
||||
export * from "https://deno.land/x/windmill@v1.195.0/windmill-api/index.ts";
|
||||
export { SEP } from "https://deno.land/std@0.201.0/path/separator.ts";
|
||||
// cliffy
|
||||
export { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.2/command/mod.ts";
|
||||
|
||||
+7
-6
@@ -44,14 +44,15 @@ async function dev(opts: GlobalOptions & { filter?: string }) {
|
||||
const content = await Deno.readTextFile(cpath);
|
||||
const splitted = cpath.split(".");
|
||||
const wmPath = splitted[0];
|
||||
const ext = splitted[splitted.length - 1];
|
||||
const len = splitted.length;
|
||||
const ext = splitted[len - 1];
|
||||
const lang =
|
||||
ext == "py"
|
||||
? "python3"
|
||||
: ext == "ts"
|
||||
? splitted.length > 2 && splitted[splitted.length - 2] == "fetch"
|
||||
? len > 2 && splitted[len - 2] == "fetch"
|
||||
? "nativets"
|
||||
: splitted.length > 2 && splitted[splitted.length - 2] == "bun"
|
||||
: len > 2 && splitted[len - 2] == "bun"
|
||||
? "bun"
|
||||
: "deno"
|
||||
: ext == "go"
|
||||
@@ -61,11 +62,11 @@ async function dev(opts: GlobalOptions & { filter?: string }) {
|
||||
: ext == "ps1"
|
||||
? "powershell"
|
||||
: ext == "sql"
|
||||
? splitted.length > 2 && splitted[splitted.length - 2] == "my"
|
||||
? len > 2 && splitted[len - 2] == "my"
|
||||
? "mysql"
|
||||
: splitted.length > 2 && splitted[splitted.length - 2] == "bq"
|
||||
: len > 2 && splitted[len - 2] == "bq"
|
||||
? "bigquery"
|
||||
: splitted.length > 2 && splitted[splitted.length - 2] == "sf"
|
||||
: len > 2 && splitted[len - 2] == "sf"
|
||||
? "snowflake"
|
||||
: "postgresql"
|
||||
: ext == "gql"
|
||||
|
||||
@@ -151,6 +151,12 @@ export async function handleFile(
|
||||
lock: typed?.lock,
|
||||
parent_hash: remote.hash,
|
||||
schema: typed?.schema,
|
||||
tag: typed?.tag,
|
||||
ws_error_handler_muted: typed?.ws_error_handler_muted,
|
||||
dedicated_worker: typed?.dedicated_worker,
|
||||
cache_ttl: typed?.cache_ttl,
|
||||
concurrency_time_window_s: typed?.concurrency_time_window_s,
|
||||
concurrent_limit: typed?.concurrent_limit,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -171,6 +177,12 @@ export async function handleFile(
|
||||
lock: typed?.lock,
|
||||
parent_hash: undefined,
|
||||
schema: typed?.schema,
|
||||
tag: typed?.tag,
|
||||
ws_error_handler_muted: typed?.ws_error_handler_muted,
|
||||
dedicated_worker: typed?.dedicated_worker,
|
||||
cache_ttl: typed?.cache_ttl,
|
||||
concurrency_time_window_s: typed?.concurrency_time_window_s,
|
||||
concurrent_limit: typed?.concurrent_limit,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { msToSec } from '$lib/utils'
|
||||
import { faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { Badge } from './common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let duration_ms: number
|
||||
</script>
|
||||
|
||||
<Badge large>
|
||||
<Icon data={faHourglassHalf} scale={0.7} class="mr-2" />
|
||||
Ran in {msToSec(duration_ms)}s<Tooltip
|
||||
>For scripts, this is the time that it took to execute from the time it was picked by a worker,
|
||||
to the time the result was stored.<br />
|
||||
For flows, it is the cumulative time of the execution of each steps. Suspend/sleep/transition times
|
||||
are not accounted for, and each step's duration in a parallel branch would be added. Hence the time
|
||||
here can differ from the time it took for the flow to have a result from start.</Tooltip
|
||||
>
|
||||
</Badge>
|
||||
@@ -444,7 +444,7 @@
|
||||
|
||||
{#if showResourceTypePicker}
|
||||
<Button
|
||||
title="Add resource"
|
||||
title="Add resource type"
|
||||
btnClasses="!font-medium text-tertiary"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { displayDate, msToSec } from '$lib/utils'
|
||||
import { displayDate } from '$lib/utils'
|
||||
import {
|
||||
faCalendar,
|
||||
faCheck,
|
||||
faCircle,
|
||||
faClock,
|
||||
faHourglassHalf,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
@@ -14,6 +13,7 @@
|
||||
import type { CompletedJob, QueuedJob } from '$lib/gen'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
import { forLater } from '$lib/forLater'
|
||||
import DurationMs from './DurationMs.svelte'
|
||||
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
@@ -26,11 +26,7 @@
|
||||
<Icon data={faCheck} scale={SMALL_ICON_SCALE} class="mr-2" />
|
||||
Success {job.is_skipped ? '(Skipped)' : ''}
|
||||
</Badge>
|
||||
|
||||
<Badge large>
|
||||
<Icon data={faHourglassHalf} scale={SMALL_ICON_SCALE} class="mr-2" />
|
||||
Ran in {msToSec(job.duration_ms)} s
|
||||
</Badge>
|
||||
<DurationMs duration_ms={job.duration_ms} />
|
||||
</div>
|
||||
{:else if job && 'success' in job}
|
||||
<div class="flex flex-row flex-wrap gap-y-1 mb-1 gap-x-2">
|
||||
@@ -38,10 +34,7 @@
|
||||
<Icon data={faTimes} scale={SMALL_ICON_SCALE} class="mr-2" />
|
||||
Failed
|
||||
</Badge>
|
||||
<Badge large>
|
||||
<Icon data={faHourglassHalf} scale={SMALL_ICON_SCALE} class="mr-2" />
|
||||
Ran in {msToSec(job.duration_ms)}s
|
||||
</Badge>
|
||||
<DurationMs duration_ms={job.duration_ms} />
|
||||
</div>
|
||||
{:else if job && 'running' in job && job.running}
|
||||
<div>
|
||||
|
||||
@@ -27,15 +27,26 @@
|
||||
}
|
||||
export let activeWorkers: number
|
||||
|
||||
let nconfig: any = config
|
||||
? config.worker_tags != undefined || config.dedicated_worker != undefined
|
||||
? config
|
||||
let nconfig: {
|
||||
dedicated_worker?: string
|
||||
worker_tags?: string[]
|
||||
priority_tags?: Map<string, number>
|
||||
cache_clear?: number
|
||||
init_bash?: string
|
||||
} = {}
|
||||
|
||||
function loadNConfig() {
|
||||
nconfig = config
|
||||
? config.worker_tags != undefined || config.dedicated_worker != undefined
|
||||
? config
|
||||
: {
|
||||
worker_tags: []
|
||||
}
|
||||
: {
|
||||
worker_tags: []
|
||||
}
|
||||
: {
|
||||
worker_tags: []
|
||||
}
|
||||
}
|
||||
|
||||
let selectedPriorityTags: string[] = []
|
||||
|
||||
const defaultTags = [
|
||||
@@ -173,7 +184,9 @@
|
||||
if (nconfig != undefined) {
|
||||
dirty = true
|
||||
nconfig.worker_tags = nconfig?.worker_tags?.filter((t) => t != tag) ?? []
|
||||
delete nconfig.priority_tags[tag]
|
||||
if (nconfig.priority_tags) {
|
||||
delete nconfig.priority_tags[tag]
|
||||
}
|
||||
selectedPriorityTags = selectedPriorityTags.filter((t) => t != tag) ?? []
|
||||
}
|
||||
}}
|
||||
@@ -264,10 +277,14 @@
|
||||
bind:selected={selectedPriorityTags}
|
||||
on:change={(e) => {
|
||||
if (e.detail.type === 'add') {
|
||||
nconfig.priority_tags[e.detail.option] = 100
|
||||
if (nconfig.priority_tags) {
|
||||
nconfig.priority_tags[e.detail.option] = 100
|
||||
}
|
||||
dirty = true
|
||||
} else if (e.detail.type === 'remove') {
|
||||
delete nconfig.priority_tags[e.detail.option]
|
||||
if (nconfig.priority_tags) {
|
||||
delete nconfig.priority_tags[e.detail.option]
|
||||
}
|
||||
dirty = true
|
||||
} else {
|
||||
console.error(
|
||||
@@ -369,7 +386,15 @@
|
||||
<div class="flex gap-2 items-center"
|
||||
><h4 class="py-4 truncate w-40 text-primary">{name}</h4>
|
||||
{#if $superadmin}
|
||||
<Button color="light" size="xs" on:click={drawer.openDrawer}>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
dirty = false
|
||||
loadNConfig()
|
||||
drawer.openDrawer()
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-1 items-center"
|
||||
>{config == undefined ? 'create' : 'edit'} config</div
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
|
||||
export let kind: 'script' | 'flow'
|
||||
export let scriptOrFlowPath: string
|
||||
@@ -71,3 +72,4 @@
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
<Tooltip>Disable workspace error handler, EE only</Tooltip>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import type { Job } from '$lib/gen'
|
||||
import { displayDate, msToSec, truncateHash, truncateRev } from '$lib/utils'
|
||||
import { displayDate, truncateHash, truncateRev } from '$lib/utils'
|
||||
import {
|
||||
faCalendar,
|
||||
faCircle,
|
||||
@@ -23,6 +23,7 @@
|
||||
import JobPreview from './JobPreview.svelte'
|
||||
import TimeAgo from '../TimeAgo.svelte'
|
||||
import { forLater } from '$lib/forLater'
|
||||
import DurationMs from '../DurationMs.svelte'
|
||||
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
@@ -141,13 +142,7 @@
|
||||
>
|
||||
</div>
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
<div>
|
||||
<Icon class="text-secondary" data={faHourglassHalf} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2"
|
||||
>
|
||||
Ran in {msToSec(job.duration_ms)}s</span
|
||||
>
|
||||
</div>
|
||||
<DurationMs duration_ms={job.duration_ms} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-secondary text-xs text-left place-self-start flex flex-col gap-1">
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
import JobArgs from '../JobArgs.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import LogViewer from '../LogViewer.svelte'
|
||||
import { msToSec } from '$lib/utils'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { Badge } from '../common'
|
||||
import { forLater } from '$lib/forLater'
|
||||
import DurationMs from '../DurationMs.svelte'
|
||||
|
||||
const POPUP_HEIGHT = 320 as const
|
||||
|
||||
@@ -105,11 +104,9 @@
|
||||
<Badge>
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
<Badge>
|
||||
<Icon class="text-secondary" data={faHourglassHalf} scale={0.5} /><span class="mx-2">
|
||||
Ran in {msToSec(job?.['duration_ms'])}s</span
|
||||
>
|
||||
</Badge>
|
||||
{#if job?.['duration_ms']}
|
||||
<DurationMs duration_ms={job?.['duration_ms']} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-auto">
|
||||
<JobArgs args={job?.args} tableClass="!pt-0 !min-w-0 !block" />
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
import { Job } from '../../gen'
|
||||
import TestJobLoader from '../TestJobLoader.svelte'
|
||||
import DisplayResult from '../DisplayResult.svelte'
|
||||
import JobArgs from '../JobArgs.svelte'
|
||||
import LogViewer from '../LogViewer.svelte'
|
||||
import { msToSec } from '$lib/utils'
|
||||
import { Badge, Skeleton, Tab, Tabs } from '../common'
|
||||
import { onMount } from 'svelte'
|
||||
import HighlightCode from '../HighlightCode.svelte'
|
||||
import { forLater } from '$lib/forLater'
|
||||
import FlowProgressBar from '../flows/FlowProgressBar.svelte'
|
||||
import FlowStatusViewer from '../FlowStatusViewer.svelte'
|
||||
import DurationMs from '../DurationMs.svelte'
|
||||
|
||||
export let id: string
|
||||
export let blankLink = false
|
||||
|
||||
let job: Job | undefined = undefined
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
let watchJob: (id: string) => Promise<void>
|
||||
let result: any
|
||||
|
||||
@@ -26,133 +23,133 @@
|
||||
result = job['result']
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
watchJob(id)
|
||||
})
|
||||
let currentJob: Job | undefined = undefined
|
||||
|
||||
onDestroy(() => {
|
||||
timeout && clearTimeout(timeout)
|
||||
})
|
||||
$: if (currentJob?.id == id) {
|
||||
job = currentJob
|
||||
}
|
||||
|
||||
$: id && watchJob && watchJob(id)
|
||||
|
||||
let viewTab = 'result'
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={({ key }) => ['Escape', 'Esc'].includes(key) && close()} />
|
||||
<TestJobLoader bind:job bind:watchJob on:done={onDone} />
|
||||
<TestJobLoader bind:job={currentJob} bind:watchJob on:done={onDone} />
|
||||
|
||||
<div class="p-4 flex flex-col gap-2 items-start">
|
||||
<div class="flex gap-2">
|
||||
{#if job?.['priority']}
|
||||
<Badge color="red">
|
||||
priority: {job?.['priority']}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
<Badge color="blue">
|
||||
Ran in ({msToSec(job.duration_ms)}s)
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job?.['mem_peak']}
|
||||
<Badge color="blue">
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
href="/run/{job?.id}?workspace={job?.workspace_id}"
|
||||
class="flex flex-row gap-1 items-center"
|
||||
target={blankLink ? '_blank' : undefined}
|
||||
>
|
||||
<span class="font-semibold text-sm leading-6">ID:</span>
|
||||
<span class="text-sm">{job?.id}</span>
|
||||
</a>
|
||||
|
||||
<span class="font-semibold text-xs leading-6">Arguments</span>
|
||||
|
||||
<div class="w-full">
|
||||
<JobArgs args={job?.args} />
|
||||
</div>
|
||||
|
||||
<span class="font-semibold text-xs leading-6">Results</span>
|
||||
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1">
|
||||
<div>Job is scheduled for</div>
|
||||
<div>{new Date(job?.['scheduled_for']).toLocaleString()}</div>
|
||||
{#if job}
|
||||
<div class="flex gap-2">
|
||||
{#if job?.['priority']}
|
||||
<Badge color="red">
|
||||
priority: {job?.['priority']}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
<DurationMs duration_ms={job.duration_ms} />
|
||||
{/if}
|
||||
{#if job?.['mem_peak']}
|
||||
<Badge large>
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<a
|
||||
href="/run/{job?.id}?workspace={job?.workspace_id}"
|
||||
class="flex flex-row gap-1 items-center"
|
||||
target={blankLink ? '_blank' : undefined}
|
||||
>
|
||||
<span class="font-semibold text-sm leading-6">ID:</span>
|
||||
<span class="text-sm">{job?.id ?? ''}</span>
|
||||
</a>
|
||||
|
||||
<div class=" w-full rounded-md overflow-auto">
|
||||
{#if job?.type === Job.type.COMPLETED_JOB}
|
||||
<Tabs bind:selected={viewTab}>
|
||||
<Tab size="xs" value="result">Result</Tab>
|
||||
<Tab size="xs" value="logs">Logs</Tab>
|
||||
{#if job?.job_kind == 'dependencies'}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{:else if job?.job_kind == 'preview'}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
<span class="font-semibold text-xs leading-6">Arguments</span>
|
||||
|
||||
<Skeleton loading={!job} layout={[[5]]} />
|
||||
{#if job}
|
||||
{#if viewTab == 'result' && (job?.job_kind == 'flow' || job?.job_kind == 'flowpreview')}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-full mt-10 mb-20">
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
<div class="w-full">
|
||||
<JobArgs args={job?.args} />
|
||||
</div>
|
||||
|
||||
<span class="font-semibold text-xs leading-6">Results</span>
|
||||
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1">
|
||||
<div>Job is scheduled for</div>
|
||||
<div>{new Date(job?.['scheduled_for']).toLocaleString()}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" w-full rounded-md overflow-auto">
|
||||
{#if job?.type === Job.type.COMPLETED_JOB}
|
||||
<Tabs bind:selected={viewTab}>
|
||||
<Tab size="xs" value="result">Result</Tab>
|
||||
<Tab size="xs" value="logs">Logs</Tab>
|
||||
{#if job?.job_kind == 'dependencies'}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{:else if job?.job_kind == 'preview'}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
|
||||
<Skeleton loading={!job} layout={[[5]]} />
|
||||
{#if job}
|
||||
{#if viewTab == 'result' && (job?.job_kind == 'flow' || job?.job_kind == 'flowpreview')}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-full mt-10 mb-20">
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row border rounded-md p-2 mt-2 max-h-1/2 overflow-auto">
|
||||
{#if viewTab == 'logs'}
|
||||
<div class="w-full">
|
||||
<LogViewer
|
||||
jobId={job.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
isLoading={!(job && 'logs' in job && job.logs)}
|
||||
content={job?.logs}
|
||||
/>
|
||||
</div>
|
||||
{:else if viewTab == 'code'}
|
||||
{#if job && 'raw_code' in job && job.raw_code}
|
||||
<div class="text-xs">
|
||||
<HighlightCode lines language={job.language} code={job.raw_code} />
|
||||
</div>
|
||||
{:else if job}
|
||||
No code is available
|
||||
{:else}
|
||||
<Skeleton layout={[[5]]} />
|
||||
{/if}
|
||||
{:else if job !== undefined && 'result' in job && job.result !== undefined}
|
||||
<DisplayResult
|
||||
workspaceId={job?.workspace_id}
|
||||
jobId={job?.id}
|
||||
{result}
|
||||
disableExpand
|
||||
/>
|
||||
{:else if job}
|
||||
No output is available yet
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if job && `running` in job ? job.running : false}
|
||||
{#if job?.job_kind == 'flow' || job?.job_kind == 'flowpreview'}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<FlowProgressBar {job} class="py-4" />
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row border rounded-md p-2 mt-2 max-h-1/2 overflow-auto">
|
||||
{#if viewTab == 'logs'}
|
||||
<div class="w-full">
|
||||
<LogViewer
|
||||
jobId={job.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
isLoading={!(job && 'logs' in job && job.logs)}
|
||||
content={job?.logs}
|
||||
/>
|
||||
</div>
|
||||
{:else if viewTab == 'code'}
|
||||
{#if job && 'raw_code' in job && job.raw_code}
|
||||
<div class="text-xs">
|
||||
<HighlightCode lines language={job.language} code={job.raw_code} />
|
||||
</div>
|
||||
{:else if job}
|
||||
No code is available
|
||||
{:else}
|
||||
<Skeleton layout={[[5]]} />
|
||||
{/if}
|
||||
{:else if job !== undefined && 'result' in job && job.result !== undefined}
|
||||
<DisplayResult
|
||||
workspaceId={job?.workspace_id}
|
||||
jobId={job?.id}
|
||||
{result}
|
||||
disableExpand
|
||||
/>
|
||||
{:else if job}
|
||||
No output is available yet
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-tertiary mb-1"> Job is still running </div>
|
||||
<LogViewer
|
||||
jobId={job?.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
content={job?.logs}
|
||||
isLoading
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if job && `running` in job ? job.running : false}
|
||||
{#if job?.job_kind == 'flow' || job?.job_kind == 'flowpreview'}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<FlowProgressBar {job} class="py-4" />
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1"> Job is still running </div>
|
||||
<LogViewer
|
||||
jobId={job?.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
content={job?.logs}
|
||||
isLoading
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -14,14 +14,8 @@
|
||||
darkMode = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-2xs text-white m-1 p-2 rounded-lg flex flex-row gap-2 justify-center hover:bg-gray-600',
|
||||
forcedDarkMode ? 'text-white hover:bg-gray-600' : 'text-primary hover:bg-surface-hover'
|
||||
)}
|
||||
on:click={() => {
|
||||
export function toggle() {
|
||||
if (!document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.add('dark')
|
||||
window.localStorage.setItem('dark-mode', 'dark')
|
||||
@@ -29,7 +23,15 @@
|
||||
document.documentElement.classList.remove('dark')
|
||||
window.localStorage.setItem('dark-mode', 'light')
|
||||
}
|
||||
}}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-2xs text-white m-1 p-2 rounded-lg flex flex-row gap-2 justify-center hover:bg-gray-600',
|
||||
forcedDarkMode ? 'text-white hover:bg-gray-600' : 'text-primary hover:bg-surface-hover'
|
||||
)}
|
||||
on:click={toggle}
|
||||
>
|
||||
{#if darkMode}
|
||||
<Sun class="w-4 h-4" />
|
||||
|
||||
@@ -594,9 +594,7 @@
|
||||
</Pane>
|
||||
<Pane size={40} minSize={15} class="border-t">
|
||||
{#if selectedId}
|
||||
{#key selectedId}
|
||||
<JobPreview id={selectedId} />
|
||||
{/key}
|
||||
<JobPreview id={selectedId} />
|
||||
{:else}
|
||||
<div class="text-xs m-4">No job selected</div>
|
||||
{/if}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
import SuperadminSettings from '$lib/components/SuperadminSettings.svelte'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import { onMount } from 'svelte'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
@@ -73,10 +72,9 @@
|
||||
$: nonAdminWorkspaces = (workspaces ?? []).filter((x) => x.id != 'admins')
|
||||
$: noWorkspaces = $superadmin && nonAdminWorkspaces.length == 0
|
||||
|
||||
onMount(() => {
|
||||
loadInvites()
|
||||
loadWorkspaces()
|
||||
})
|
||||
loadInvites()
|
||||
loadWorkspaces()
|
||||
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
|
||||
@@ -72,12 +72,13 @@
|
||||
secondInterval = setInterval(() => {
|
||||
timeSinceLastPing += 1
|
||||
}, 1000)
|
||||
if ($superadmin) {
|
||||
loadGlobalCache()
|
||||
loadCustomTags()
|
||||
}
|
||||
})
|
||||
|
||||
$: if ($superadmin) {
|
||||
loadGlobalCache()
|
||||
loadCustomTags()
|
||||
}
|
||||
|
||||
async function loadGlobalCache() {
|
||||
try {
|
||||
globalCache = (await SettingService.getGlobal({ key: WORKER_S3_BUCKET_SYNC_SETTING })) ?? true
|
||||
@@ -103,10 +104,10 @@
|
||||
}
|
||||
})
|
||||
|
||||
let newGroupName = ''
|
||||
let newConfigName = ''
|
||||
|
||||
async function addGroup() {
|
||||
await ConfigService.updateConfig({ name: 'worker__' + newGroupName, requestBody: {} })
|
||||
async function addConfig() {
|
||||
await ConfigService.updateConfig({ name: 'worker__' + newConfigName, requestBody: {} })
|
||||
loadWorkerGroups()
|
||||
}
|
||||
|
||||
@@ -262,7 +263,7 @@
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col gap-2">
|
||||
<input class="mr-2 h-full" placeholder="New group name" bind:value={newGroupName} />
|
||||
<input class="mr-2 h-full" placeholder="New group name" bind:value={newConfigName} />
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="flex items-center whitespace-nowrap text-yellow-600 gap-2">
|
||||
@@ -273,8 +274,8 @@
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
disabled={!newGroupName || !$enterpriseLicense}
|
||||
on:click={addGroup}
|
||||
disabled={!newConfigName || !$enterpriseLicense}
|
||||
on:click={addConfig}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
@@ -283,7 +284,7 @@
|
||||
</div>
|
||||
{/if}</div
|
||||
>
|
||||
{#each groupedWorkers as worker_group}
|
||||
{#each groupedWorkers as worker_group (worker_group[0])}
|
||||
<WorkspaceGroup
|
||||
name={worker_group[0]}
|
||||
config={(workerGroups ?? {})[worker_group[0]]}
|
||||
@@ -365,7 +366,7 @@
|
||||
|
||||
<div class="pb-4" />
|
||||
|
||||
{#each Object.entries(workerGroups ?? {}).filter((x) => !groupedWorkers.some((y) => y[0] == x[0])) as worker_group}
|
||||
{#each Object.entries(workerGroups ?? {}).filter((x) => !groupedWorkers.some((y) => y[0] == x[0])) as worker_group (worker_group[0])}
|
||||
<WorkspaceGroup
|
||||
on:reload={() => {
|
||||
loadWorkerGroups()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { OpenAPI, SettingsService, UserService, WorkspaceService } from '$lib/gen'
|
||||
import { SettingsService, UserService, WorkspaceService } from '$lib/gen'
|
||||
import { logoutWithRedirect } from '$lib/logout'
|
||||
import { enterpriseLicense, userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
|
||||
import { getUserExt } from '$lib/user'
|
||||
@@ -12,12 +12,6 @@
|
||||
import EditorTheme from '$lib/components/EditorTheme.svelte'
|
||||
import { computeDrift } from '$lib/forLater'
|
||||
|
||||
let token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
if (token) {
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
OpenAPI.TOKEN = $page.url.searchParams.get('wm_token')!
|
||||
}
|
||||
|
||||
const monacoEditorUnhandledErrors = [
|
||||
'Model not found',
|
||||
'Connection is disposed.',
|
||||
|
||||
+48
-9
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { onDestroy, onMount, setContext } from 'svelte'
|
||||
@@ -8,18 +6,52 @@
|
||||
import FlowPreviewButtons from '$lib/components/flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext } from '$lib/components/flows/types'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { FlowModule, Job, OpenFlow } from '$lib/gen'
|
||||
import { OpenAPI, type FlowModule, type OpenFlow } from '$lib/gen'
|
||||
import { initHistory } from '$lib/history'
|
||||
import type { FlowState } from '$lib/components/flows/flowState'
|
||||
import FlowModuleSchemaMap from '$lib/components/flows/map/FlowModuleSchemaMap.svelte'
|
||||
import FlowEditorPanel from '$lib/components/flows/content/FlowEditorPanel.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { page } from '$app/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
|
||||
let testJobLoader: TestJobLoader
|
||||
let token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
let workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
let themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
|
||||
let themeDark = themeDarkRaw == '2' || themeDarkRaw == '4'
|
||||
|
||||
// Test
|
||||
let testIsLoading = false
|
||||
let testJob: Job | undefined
|
||||
if (token) {
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
OpenAPI.TOKEN = $page.url.searchParams.get('wm_token')!
|
||||
}
|
||||
|
||||
if (workspace) {
|
||||
$workspaceStore = workspace
|
||||
}
|
||||
|
||||
if (workspace && token) {
|
||||
loadUser()
|
||||
}
|
||||
|
||||
async function loadUser() {
|
||||
const user = await getUserExt(workspace!)
|
||||
userStore.set(user)
|
||||
}
|
||||
|
||||
let darkModeToggle: DarkModeToggle
|
||||
let darkMode: boolean | undefined = undefined
|
||||
let modeInitialized = false
|
||||
function initializeMode() {
|
||||
modeInitialized = true
|
||||
darkModeToggle.toggle()
|
||||
}
|
||||
$: darkModeToggle &&
|
||||
themeDark != darkMode &&
|
||||
darkMode != undefined &&
|
||||
!modeInitialized &&
|
||||
initializeMode()
|
||||
|
||||
const flowStore = writable({
|
||||
summary: '',
|
||||
@@ -188,8 +220,6 @@
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job={testJob} />
|
||||
|
||||
<main class="h-screen w-full">
|
||||
<div class="h-full w-full grid grid-cols-2">
|
||||
<SimpleEditor
|
||||
@@ -201,6 +231,15 @@
|
||||
}}
|
||||
/>
|
||||
<div class="flex flex-col max-h-screen h-full relative">
|
||||
<div class="absolute top-0 left-2">
|
||||
<DarkModeToggle bind:darkMode bind:this={darkModeToggle} forcedDarkMode={false} />
|
||||
{#if $userStore}
|
||||
As {$userStore?.username} in {$workspaceStore}
|
||||
{:else}
|
||||
<span class="text-red-600">Unable to login</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-1 z-50 absolute right-2 top-2 gap-2">
|
||||
<FlowPreviewButtons />
|
||||
</div>
|
||||
@@ -0,0 +1,273 @@
|
||||
<script lang="ts">
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
import { Button, Kbd } from '$lib/components/common'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import LogPanel from '$lib/components/scriptEditor/LogPanel.svelte'
|
||||
import { CompletedJob, Job, JobService, OpenAPI, Preview } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, getModifierKey, sendUserToast } from '$lib/utils'
|
||||
import { faPlay } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import { getUserExt } from '$lib/user'
|
||||
|
||||
let token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
let workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
let themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
|
||||
let themeDark = themeDarkRaw == '2' || themeDarkRaw == '4'
|
||||
|
||||
if (token) {
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
OpenAPI.TOKEN = $page.url.searchParams.get('wm_token')!
|
||||
}
|
||||
|
||||
if (workspace) {
|
||||
$workspaceStore = workspace
|
||||
}
|
||||
|
||||
if (workspace && token) {
|
||||
loadUser()
|
||||
}
|
||||
|
||||
async function loadUser() {
|
||||
const user = await getUserExt(workspace!)
|
||||
userStore.set(user)
|
||||
}
|
||||
|
||||
let darkModeToggle: DarkModeToggle
|
||||
let darkMode: boolean | undefined = undefined
|
||||
let modeInitialized = false
|
||||
function initializeMode() {
|
||||
modeInitialized = true
|
||||
darkModeToggle.toggle()
|
||||
}
|
||||
$: darkModeToggle &&
|
||||
themeDark != darkMode &&
|
||||
darkMode != undefined &&
|
||||
!modeInitialized &&
|
||||
initializeMode()
|
||||
|
||||
let testJobLoader: TestJobLoader
|
||||
|
||||
// Test args input
|
||||
let args: Record<string, any> = {}
|
||||
let isValid: boolean = true
|
||||
|
||||
// Test
|
||||
let testIsLoading = false
|
||||
let testJob: Job | undefined
|
||||
let pastPreviews: CompletedJob[] = []
|
||||
let validCode = true
|
||||
|
||||
type LastEdit = {
|
||||
content: string
|
||||
path: string
|
||||
language: Preview.language
|
||||
}
|
||||
|
||||
let currentScript: LastEdit | undefined = undefined
|
||||
|
||||
let schema = emptySchema()
|
||||
const href = window.location.href
|
||||
const indexQ = href.indexOf('?')
|
||||
const searchParams = indexQ > -1 ? new URLSearchParams(href.substring(indexQ)) : undefined
|
||||
|
||||
if (searchParams?.has('local')) {
|
||||
connectWs()
|
||||
}
|
||||
|
||||
const el = (event) => {
|
||||
if (event.data.type == 'runTest') {
|
||||
runTest()
|
||||
} else if (event.data.type == 'replaceScript') {
|
||||
replaceScript(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('message', el, false)
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const obj = {
|
||||
altKey: e.altKey,
|
||||
code: e.code,
|
||||
ctrlKey: e.ctrlKey,
|
||||
isComposing: e.isComposing,
|
||||
key: e.key,
|
||||
location: e.location,
|
||||
metaKey: e.metaKey,
|
||||
repeat: e.repeat,
|
||||
shiftKey: e.shiftKey
|
||||
}
|
||||
window.parent?.postMessage(JSON.stringify(obj), '*')
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('message', el)
|
||||
})
|
||||
|
||||
function connectWs() {
|
||||
const port = searchParams?.get('port') || '3001'
|
||||
try {
|
||||
const socket = new WebSocket(`ws://localhost:${port}/ws`)
|
||||
|
||||
// Listen for messages
|
||||
socket.addEventListener('message', (event) => {
|
||||
replaceData(event.data)
|
||||
})
|
||||
|
||||
function replaceData(msg: string) {
|
||||
let data: any | undefined = undefined
|
||||
try {
|
||||
data = JSON.parse(msg)
|
||||
} catch {
|
||||
console.log('Received invalid JSON: ' + msg)
|
||||
return
|
||||
}
|
||||
replaceScript(data)
|
||||
}
|
||||
} catch (e) {
|
||||
sendUserToast('Failed to connect to local server', true)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
if (!currentScript) {
|
||||
return
|
||||
}
|
||||
//@ts-ignore
|
||||
testJobLoader.runPreview(
|
||||
currentScript.path,
|
||||
currentScript.content,
|
||||
currentScript.language,
|
||||
args,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
async function loadPastTests(): Promise<void> {
|
||||
if (!currentScript) {
|
||||
return
|
||||
}
|
||||
console.log('Loading past tests')
|
||||
pastPreviews = await JobService.listCompletedJobs({
|
||||
workspace: $workspaceStore!,
|
||||
jobKinds: 'preview',
|
||||
createdBy: $userStore?.username,
|
||||
scriptPathExact: currentScript.path
|
||||
})
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
|
||||
event.preventDefault()
|
||||
runTest()
|
||||
}
|
||||
}
|
||||
|
||||
let lastPath: string | undefined = undefined
|
||||
async function replaceScript(lastEdit: LastEdit) {
|
||||
currentScript = lastEdit
|
||||
if (lastPath !== lastEdit.path) {
|
||||
schema = emptySchema()
|
||||
}
|
||||
try {
|
||||
await inferArgs(lastEdit.language, lastEdit.content, schema)
|
||||
schema = schema
|
||||
lastPath = lastEdit.path
|
||||
validCode = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
validCode = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<TestJobLoader
|
||||
on:done={loadPastTests}
|
||||
bind:this={testJobLoader}
|
||||
bind:isLoading={testIsLoading}
|
||||
bind:job={testJob}
|
||||
/>
|
||||
|
||||
<main class="h-screen w-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="absolute top-0 left-2">
|
||||
<DarkModeToggle bind:darkMode bind:this={darkModeToggle} forcedDarkMode={false} />
|
||||
</div>
|
||||
<div class="text-center w-full text-lg truncate py-1 text-primary">
|
||||
{currentScript?.path ?? 'Not editing a script'}
|
||||
{currentScript?.language ?? ''}
|
||||
</div>
|
||||
<div class="absolute top-2 right-2 !text-tertiary text-xs">
|
||||
{#if $userStore}
|
||||
As {$userStore?.username} in {$workspaceStore}
|
||||
{:else}
|
||||
<span class="text-red-600">Unable to login</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !validCode}
|
||||
<div class="text-center w-full text-lg truncate py-1 text-red-500">Invalid code</div>
|
||||
{/if}
|
||||
<div class="flex justify-center pt-1">
|
||||
{#if testIsLoading}
|
||||
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
|
||||
<WindmillIcon
|
||||
white={true}
|
||||
class="mr-2 text-white"
|
||||
height="20px"
|
||||
width="20px"
|
||||
spin="fast"
|
||||
/>
|
||||
Cancel
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
disabled={currentScript === undefined}
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
runTest()
|
||||
}}
|
||||
btnClasses="w-full"
|
||||
size="xs"
|
||||
startIcon={{
|
||||
icon: faPlay,
|
||||
classes: 'animate-none'
|
||||
}}
|
||||
>
|
||||
{#if testIsLoading}
|
||||
Running
|
||||
{:else}
|
||||
Test <Kbd small isModifier>{getModifierKey()}</Kbd>
|
||||
<Kbd small><span class="text-lg font-bold">⏎</span></Kbd>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={33}>
|
||||
<div class="px-2">
|
||||
<div class="break-words relative font-sans">
|
||||
<SchemaForm compact {schema} bind:args bind:isValid />
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={67}>
|
||||
<LogPanel
|
||||
lang={currentScript?.language}
|
||||
previewJob={testJob}
|
||||
{pastPreviews}
|
||||
previewIsLoading={testIsLoading}
|
||||
/>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</main>
|
||||
+5
@@ -11,6 +11,7 @@
|
||||
import { faPlay } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
|
||||
let testJobLoader: TestJobLoader
|
||||
|
||||
@@ -160,10 +161,14 @@
|
||||
|
||||
<main class="h-screen w-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="absolute top-0 right-2">
|
||||
<DarkModeToggle forcedDarkMode={false} />
|
||||
</div>
|
||||
<div class="text-center w-full text-lg truncate py-1 dark:text-white">
|
||||
{currentScript?.path ?? 'Not editing a script'}
|
||||
{currentScript?.language ?? ''}
|
||||
</div>
|
||||
|
||||
{#if !validCode}
|
||||
<div class="text-center w-full text-lg truncate py-1 text-red-500">Invalid code</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user