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:
Ruben Fiszel
2023-11-02 18:52:40 +01:00
committed by GitHub
parent 86a2ced605
commit a75ae3bfdb
22 changed files with 558 additions and 206 deletions
+1 -1
View File
@@ -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
View File
@@ -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"
+12
View File
@@ -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>
+1 -1
View File
@@ -444,7 +444,7 @@
{#if showResourceTypePicker}
<Button
title="Add resource"
title="Add resource type"
btnClasses="!font-medium text-tertiary"
size="xs"
spacingSize="md"
+4 -11
View File
@@ -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" />
+114 -117
View File
@@ -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
View File
@@ -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.',
@@ -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&nbsp;<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>
@@ -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}