mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 16:02:11 +00:00
feat: add support for flows in vscode extension (#2585)
* all * change tagline * all * all * all
This commit is contained in:
@@ -524,7 +524,14 @@ pub async fn add_completed_job<
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = send_error_to_workspace_handler(rsmq.clone(), &queued_job, canceled_by.is_some(), db, result).await
|
||||
if let Err(e) = send_error_to_workspace_handler(
|
||||
rsmq.clone(),
|
||||
&queued_job,
|
||||
canceled_by.is_some(),
|
||||
db,
|
||||
result,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Could not run workspace error handler for job {}: {}",
|
||||
@@ -2623,7 +2630,7 @@ pub async fn push<'c, T: Serialize + Send + Sync, R: rsmq_async::RsmqConnection
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id}: {e}")))?;
|
||||
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id} with tag {tag}, schedule_path {schedule_path:?}, script_path: {script_path:?}, email {email}, workspace_id {workspace_id}: {e}")))?;
|
||||
|
||||
// TODO: technically the job isn't queued yet, as the transaction can be rolled back. Should be solved when moving these metrics to the queue abstraction.
|
||||
if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
import { BROWSER } from 'esm-env'
|
||||
import { premiumStore, userStore, workspaceStore } from './stores'
|
||||
import { getUserExt } from './user'
|
||||
import { WorkspaceService } from './gen'
|
||||
|
||||
export function isCloudHosted(): boolean {
|
||||
return BROWSER && window.location.hostname == 'app.windmill.dev'
|
||||
}
|
||||
|
||||
if (BROWSER) {
|
||||
workspaceStore.subscribe(async (workspace) => {
|
||||
if (workspace) {
|
||||
try {
|
||||
localStorage.setItem('workspace', String(workspace))
|
||||
} catch (e) {
|
||||
console.error('Could not persist workspace to local storage', e)
|
||||
}
|
||||
const user = await getUserExt(workspace)
|
||||
userStore.set(user)
|
||||
if (isCloudHosted() && user?.is_admin) {
|
||||
premiumStore.set(await WorkspaceService.getPremiumInfo({ workspace }))
|
||||
}
|
||||
} else {
|
||||
userStore.set(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
<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,
|
||||
type OpenFlow,
|
||||
type FlowModule
|
||||
} 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, setContext } from 'svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte'
|
||||
import FlowEditorPanel from './flows/content/FlowEditorPanel.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { initHistory } from '$lib/history'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
|
||||
$: token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
$: themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
|
||||
$: 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 = 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 LastEditScript = {
|
||||
content: string
|
||||
path: string
|
||||
language: Preview.language
|
||||
}
|
||||
|
||||
let currentScript: LastEditScript | 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()
|
||||
}
|
||||
|
||||
let lockChanges = false
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
const el = (event) => {
|
||||
// sendUserToast(`Received message from parent ${event.data.type}`, true)
|
||||
if (event.data.type == 'runTest') {
|
||||
runTest()
|
||||
} else if (event.data.type == 'replaceScript') {
|
||||
mode = 'script'
|
||||
replaceScript(event.data)
|
||||
} else if (event.data.type == 'replaceFlow') {
|
||||
mode = 'flow'
|
||||
lockChanges = true
|
||||
replaceFlow(event.data)
|
||||
timeout && clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
lockChanges = false
|
||||
}, 500)
|
||||
} else if (event.data.type == 'error') {
|
||||
sendUserToast(event.data.error.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (obj.ctrlKey && obj.key == 'a') {
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
window.parent?.postMessage({ type: 'keydown', key: JSON.stringify(obj) }, '*')
|
||||
})
|
||||
window.parent?.postMessage({ type: 'refresh' }, '*')
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('message', el)
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket?.close()
|
||||
}
|
||||
})
|
||||
|
||||
let socket: WebSocket | undefined = undefined
|
||||
function connectWs() {
|
||||
const port = searchParams?.get('port') || '3001'
|
||||
try {
|
||||
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 (mode == 'script') {
|
||||
if (!currentScript) {
|
||||
return
|
||||
}
|
||||
//@ts-ignore
|
||||
testJobLoader.runPreview(
|
||||
currentScript.path,
|
||||
currentScript.content,
|
||||
currentScript.language,
|
||||
args,
|
||||
undefined
|
||||
)
|
||||
} else {
|
||||
flowPreviewButtons?.openPreview()
|
||||
}
|
||||
}
|
||||
|
||||
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: LastEditScript) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
let mode: 'script' | 'flow' = 'script'
|
||||
|
||||
const flowStore = writable({
|
||||
summary: '',
|
||||
value: { modules: [] },
|
||||
extra_perms: {},
|
||||
schema: emptySchema()
|
||||
} as OpenFlow)
|
||||
|
||||
type LastEditFlow = {
|
||||
flow: OpenFlow
|
||||
uriPath: string
|
||||
}
|
||||
let lastUriPath: string | undefined = undefined
|
||||
async function replaceFlow(lastEdit: LastEditFlow) {
|
||||
lastUriPath = lastEdit.uriPath
|
||||
// sendUserToast(JSON.stringify(lastEdit.flow), true)
|
||||
// return
|
||||
try {
|
||||
if (!deepEqual(lastEdit.flow, $flowStore)) {
|
||||
$flowStore = lastEdit.flow
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('issue setting new flowstore', e)
|
||||
}
|
||||
}
|
||||
|
||||
const flowStateStore = writable({} as FlowState)
|
||||
const scheduleStore = writable({
|
||||
args: {},
|
||||
cron: '',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: false
|
||||
})
|
||||
const previewArgsStore = writable<Record<string, any>>({})
|
||||
const scriptEditorDrawer = writable(undefined)
|
||||
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
|
||||
const history = initHistory($flowStore)
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
moving,
|
||||
history,
|
||||
pathStore: writable(''),
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
testStepStore,
|
||||
saveDraft: () => {},
|
||||
initialPath: ''
|
||||
})
|
||||
|
||||
$: updateCode($flowStore)
|
||||
|
||||
let lastSent: OpenFlow | undefined = undefined
|
||||
function updateCode(flow: OpenFlow) {
|
||||
if (lockChanges) {
|
||||
return
|
||||
}
|
||||
if (!deepEqual(flow, lastSent)) {
|
||||
lastSent = JSON.parse(JSON.stringify(flow))
|
||||
window?.parent.postMessage({ type: 'flow', flow, uriPath: lastUriPath }, '*')
|
||||
}
|
||||
}
|
||||
|
||||
let flowPreviewButtons: FlowPreviewButtons
|
||||
</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">
|
||||
{#if mode == 'script'}
|
||||
<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 != undefined}
|
||||
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>
|
||||
{:else}
|
||||
<!-- <div class="h-full w-full grid grid-cols-2"> -->
|
||||
<div class="h-full w-full">
|
||||
<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 on {$workspaceStore}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-1 z-50 absolute right-2 top-2 gap-2">
|
||||
<FlowPreviewButtons bind:this={flowPreviewButtons} />
|
||||
</div>
|
||||
<Splitpanes horizontal class="h-full max-h-screen grow">
|
||||
<Pane size={67}>
|
||||
{#if $flowStore?.value?.modules}
|
||||
<FlowModuleSchemaMap
|
||||
bind:modules={$flowStore.value.modules}
|
||||
disableAi
|
||||
disableTutorials
|
||||
smallErrorHandler={true}
|
||||
disableStaticInputs
|
||||
/>
|
||||
{:else}
|
||||
<div class="text-red-400 mt-20">Missing flow modules</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={33}>
|
||||
<FlowEditorPanel noEditor />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
@@ -40,6 +40,10 @@
|
||||
let isRunning: boolean = false
|
||||
let jobProgressReset: () => void
|
||||
|
||||
export function test() {
|
||||
runPreview($previewArgs, undefined)
|
||||
}
|
||||
|
||||
const { selectedId, previewArgs, flowStateStore, flowStore, pathStore, initialPath } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -97,14 +97,21 @@
|
||||
// If editing the arg's name, oldName containing the old argument name must be provided
|
||||
argError = ''
|
||||
modalProperty.name = modalProperty.name.trim()
|
||||
|
||||
if (modalProperty.name.length === 0) {
|
||||
argError = 'Arguments need to have a name'
|
||||
} else if (
|
||||
Object.keys(schema.properties).includes(modalProperty.name) &&
|
||||
Object.keys(schema.properties ?? {}).includes(modalProperty.name) &&
|
||||
(!editing || (editing && oldArgName && oldArgName !== modalProperty.name))
|
||||
) {
|
||||
argError = 'There is already an argument with this name'
|
||||
} else {
|
||||
if (!schema.properties) {
|
||||
schema.properties = {}
|
||||
}
|
||||
if (!schema.required) {
|
||||
schema.required = []
|
||||
}
|
||||
schema.properties[modalProperty.name] = modalToSchema(modalProperty)
|
||||
if (modalProperty.required) {
|
||||
if (!schema.required.includes(modalProperty.name)) {
|
||||
@@ -126,10 +133,12 @@
|
||||
|
||||
schemaModal.closeDrawer()
|
||||
}
|
||||
|
||||
schema = schema
|
||||
syncOrders()
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
jsonEditor?.setCode(schemaString)
|
||||
sendUserToast('FOO')
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
let previewOpen = false
|
||||
let previewMode: 'upTo' | 'whole' = 'whole'
|
||||
|
||||
export async function openPreview() {
|
||||
if (!previewOpen) {
|
||||
previewOpen = true
|
||||
} else {
|
||||
flowPreviewContent?.test()
|
||||
}
|
||||
}
|
||||
|
||||
let flowPreviewContent: FlowPreviewContent
|
||||
let jobId: string | undefined = undefined
|
||||
let job: Job | undefined = undefined
|
||||
|
||||
@@ -73,6 +82,7 @@
|
||||
|
||||
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
|
||||
<FlowPreviewContent
|
||||
bind:this={flowPreviewContent}
|
||||
open={previewOpen}
|
||||
bind:previewMode
|
||||
bind:job
|
||||
|
||||
@@ -9,13 +9,21 @@
|
||||
OpenAPI,
|
||||
RawAppService,
|
||||
ScriptService,
|
||||
UserService
|
||||
UserService,
|
||||
WorkspaceService
|
||||
} from '$lib/gen'
|
||||
import { classNames } from '$lib/utils'
|
||||
|
||||
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
|
||||
import SidebarContent from '$lib/components/sidebar/SidebarContent.svelte'
|
||||
import { starStore, superadmin, usageStore, userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
premiumStore,
|
||||
starStore,
|
||||
superadmin,
|
||||
usageStore,
|
||||
userStore,
|
||||
workspaceStore
|
||||
} from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation'
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
@@ -26,6 +34,7 @@
|
||||
import { SUPERADMIN_SETTINGS_HASH, USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { syncTutorialsTodos } from '$lib/tutorialUtils'
|
||||
import { getUserExt } from '$lib/user'
|
||||
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
let menuOpen = false
|
||||
@@ -50,6 +59,25 @@
|
||||
superadminSettings.openDrawer()
|
||||
}
|
||||
|
||||
$: updateUserStore($workspaceStore)
|
||||
|
||||
async function updateUserStore(workspace: string | undefined) {
|
||||
if (workspace) {
|
||||
try {
|
||||
localStorage.setItem('workspace', String(workspace))
|
||||
} catch (e) {
|
||||
console.error('Could not persist workspace to local storage', e)
|
||||
}
|
||||
const user = await getUserExt(workspace)
|
||||
userStore.set(user)
|
||||
if (isCloudHosted() && user?.is_admin) {
|
||||
premiumStore.set(await WorkspaceService.getPremiumInfo({ workspace }))
|
||||
}
|
||||
} else {
|
||||
userStore.set(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
beforeNavigate(() => {
|
||||
menuOpen = false
|
||||
})
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Dev from '$lib/components/Dev.svelte'
|
||||
</script>
|
||||
|
||||
<Dev />
|
||||
@@ -1,273 +1,5 @@
|
||||
<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
|
||||
}
|
||||
}
|
||||
import Dev from '$lib/components/Dev.svelte'
|
||||
</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>
|
||||
<Dev />
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
<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, 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'
|
||||
|
||||
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 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}
|
||||
<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>
|
||||
Reference in New Issue
Block a user