mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat(frontend): add quick access menu in flow editor (#4415)
* (frontend) add quick access menu in flow editor * (frontend) add quick access menu in flow editor * improve UI * make design prettier * add scroll effects * improve loading preview * change no items found * prevent scroll using menu * change user folder button * set default integration icon * reduce column width * ajust font * add defaults script button * add shadow divider * fix scroll * add chevron * Change toogle bar * Add preprocessor menu * add handler * simplify scroll * fix display * fix minor issues * delete useless log * revert node tree changes * merge main * fix z-index issues * iterate * fix: improve allowed domains setting for sso * chore(main): release 1.402.3 (#4458) * chore(main): release 1.402.3 * Apply automatic changes --------- Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com> * improve allowed domains change handling * send stats when renewing key if last >24h (#4430) * feat: send stats when renewing key if last >24h * nits * fix: sqlx * nit * renewal reason * stats reason * update ee ref * Update ee-repo-ref.txt --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev> * fix: skip one migration to avoid using md5 for azure support * all * all * Apply automatic changes * all * done? * nit * nit * nit * nit * nit noAi if prefilter is not all * fix shadow * fix error handler * fix error handler * Polishing default script settings * all * all * full * all * add deno_core as features * all * remove warnings * all * npm check * npm check * new script script * nits * nits item 0 * nits item 0 --------- Co-authored-by: Guilhem Le Mouel <guilhem.le-mouel.ext@altran.com> Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com> Co-authored-by: HugoCasa <hugo@casademont.ch> Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE SCRIPT_KIND ADD VALUE IF NOT EXISTS 'preprocessor';
|
||||
@@ -177,6 +177,11 @@
|
||||
"svelte": "./package/components/icons/WindmillIcon.svelte",
|
||||
"default": "./package/components/icons/WindmillIcon.svelte"
|
||||
},
|
||||
"./components/icons/WindmillIcon2.svelte": {
|
||||
"types": "./package/components/icons/WindmillIcon2.d.ts",
|
||||
"svelte": "./package/components/icons/WindmillIcon2.svelte",
|
||||
"default": "./package/components/icons/WindmillIcon2.svelte"
|
||||
},
|
||||
"./components/IconedResourceType.svelte": {
|
||||
"types": "./package/components/IconedResourceType.svelte.d.ts",
|
||||
"svelte": "./package/components/IconedResourceType.svelte",
|
||||
@@ -358,6 +363,9 @@
|
||||
"components/icons/WindmillIcon.svelte": [
|
||||
"./package/components/icons/WindmillIcon.svelte.d.ts"
|
||||
],
|
||||
"components/icons/WindmillIcon2.svelte": [
|
||||
"./package/components/icons/WindmillIcon2.svelte.d.ts"
|
||||
],
|
||||
"components/scriptEditor/LogPanel.svelte": [
|
||||
"./package/components/scriptEditor/LogPanel.svelte.d.ts"
|
||||
],
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
import DefaultScriptsInner from './DefaultScriptsInner.svelte'
|
||||
|
||||
let drawer: Drawer
|
||||
export let placement: 'left' | 'right' = 'left'
|
||||
|
||||
export let size: 'xs3' | 'xs2' = 'xs2'
|
||||
export let noText = false
|
||||
</script>
|
||||
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Drawer bind:this={drawer} placement="left">
|
||||
<Drawer bind:this={drawer} {placement}>
|
||||
<DrawerContent title="Edit Default Scripts" on:close={drawer.closeDrawer}>
|
||||
<DefaultScriptsInner />
|
||||
</DrawerContent>
|
||||
@@ -19,8 +23,10 @@
|
||||
on:click={drawer?.openDrawer}
|
||||
startIcon={{ icon: SettingsIcon }}
|
||||
color="light"
|
||||
size="xs2"
|
||||
{size}
|
||||
btnClasses="!text-tertiary"
|
||||
variant="contained">defaults</Button
|
||||
variant="contained"
|
||||
>
|
||||
{noText ? '' : 'defaults'}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { defaultScriptLanguages } from '$lib/scripts'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
|
||||
export let small = false
|
||||
$: langs = computeLangs($defaultScripts)
|
||||
|
||||
function computeLangs(defaultScripts: WorkspaceDefaultScripts | undefined): Script['language'][] {
|
||||
@@ -31,24 +32,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Alert title="Global to workspace" type="info" class="mb-4">
|
||||
<Alert title="Global to workspace" type="info" class="mb-4" size={small ? 'xs' : 'sm'}>
|
||||
This setting is only available to admins and will affect all users in the workspace.
|
||||
</Alert>
|
||||
<div class="h-full w-full flex-col gap-2 flex">
|
||||
<div class="h-full w-full flex-col {small ? 'gap-0' : 'gap-2'} flex">
|
||||
{#each langs as lang, i (lang)}
|
||||
<div
|
||||
animate:flip={{ duration: 300 }}
|
||||
class="w-full p-2 rounded border border-seconadry grid grid-cols-3"
|
||||
><h3>{lang}</h3>
|
||||
class="w-full p-2 rounded {small
|
||||
? ''
|
||||
: 'border border-secondary'} grid grid-cols-3 items-center"
|
||||
><h3 class={small ? 'text-xs font-medium justify-center' : ''}>{lang}</h3>
|
||||
<div>
|
||||
{#if i > 0}
|
||||
<button on:click={() => changePosition(i ?? 0, true)} class="text-lg mr-2">
|
||||
<button
|
||||
on:click={() => changePosition(i ?? 0, true)}
|
||||
class={small ? 'mr-2 text-secondary text-sm' : 'text-lg mr-2'}
|
||||
title="Move up"
|
||||
>
|
||||
↑</button
|
||||
>
|
||||
{/if}
|
||||
{#if i < langs.length - 1}
|
||||
<button on:click={() => changePosition(i ?? 0, false)} class="text-lg mr-2"
|
||||
>↓</button
|
||||
<button
|
||||
on:click={() => changePosition(i ?? 0, false)}
|
||||
class={small ? 'mr-2 text-secondary text-sm' : 'text-lg mr-2'}
|
||||
title="Move down">↓</button
|
||||
>
|
||||
{/if}</div
|
||||
>
|
||||
|
||||
@@ -496,7 +496,8 @@
|
||||
saveDraft: () => {},
|
||||
initialPath: '',
|
||||
flowInputsStore: writable<FlowInput>({}),
|
||||
customUi: {}
|
||||
customUi: {},
|
||||
insertButtonOpen: writable(false)
|
||||
})
|
||||
|
||||
$: updateFlow($flowStore)
|
||||
|
||||
@@ -394,6 +394,7 @@
|
||||
selectedIdStore.set(selectedId)
|
||||
}
|
||||
|
||||
let insertButtonOpen = writable<boolean>(false)
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
schedule: scheduleStore,
|
||||
@@ -408,7 +409,8 @@
|
||||
saveDraft,
|
||||
initialPath,
|
||||
flowInputsStore: writable<FlowInput>({}),
|
||||
customUi
|
||||
customUi,
|
||||
insertButtonOpen
|
||||
})
|
||||
|
||||
async function loadSchedule() {
|
||||
@@ -461,20 +463,24 @@
|
||||
}
|
||||
break
|
||||
case 'ArrowDown': {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
if (idx > -1 && idx < ids.length - 1) {
|
||||
$selectedIdStore = ids[idx + 1]
|
||||
event.preventDefault()
|
||||
if (!$insertButtonOpen) {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
if (idx > -1 && idx < ids.length - 1) {
|
||||
$selectedIdStore = ids[idx + 1]
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
if (idx > 0 && idx < ids.length) {
|
||||
$selectedIdStore = ids[idx - 1]
|
||||
event.preventDefault()
|
||||
if (!$insertButtonOpen) {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
if (idx > 0 && idx < ids.length) {
|
||||
$selectedIdStore = ids[idx - 1]
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -561,6 +567,8 @@
|
||||
kind: string
|
||||
app: string
|
||||
ask_id: number
|
||||
id: number
|
||||
version_id: number
|
||||
}[]
|
||||
} catch (err) {
|
||||
if (err.name !== 'CancelError') throw err
|
||||
@@ -890,7 +898,8 @@
|
||||
const snakeKey = snakeCase(key)
|
||||
if (
|
||||
schemaProperty &&
|
||||
(!$flowStore.schema || !(snakeKey in ($flowStore.schema.properties as any) ?? {})) // prevent overriding flow inputs
|
||||
(!$flowStore.schema ||
|
||||
!(snakeKey in ($flowStore?.schema?.properties ?? ({} as any)))) // prevent overriding flow inputs
|
||||
) {
|
||||
copilotFlowInputs[snakeKey] = schemaProperty
|
||||
if (schema?.required.includes(snakeKey)) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
let isAtBottom: boolean = false
|
||||
let isScrollable = false
|
||||
|
||||
export let scrollableClass: string = ''
|
||||
export let shiftedShadow: boolean = false
|
||||
let mutationObserver: MutationObserver
|
||||
let el: HTMLDivElement
|
||||
|
||||
function handleScroll(event) {
|
||||
const scrollableElement = event.target
|
||||
|
||||
isAtBottom =
|
||||
scrollableElement.scrollTop + scrollableElement.offsetHeight >=
|
||||
scrollableElement.scrollHeight - 2
|
||||
}
|
||||
|
||||
function checkIfScrollable(el) {
|
||||
return el.scrollHeight > el.clientHeight
|
||||
}
|
||||
|
||||
function observeScrollability(el) {
|
||||
isScrollable = checkIfScrollable(el)
|
||||
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
isScrollable = checkIfScrollable(el)
|
||||
})
|
||||
mutationObserver?.observe(el, { childList: true, subtree: true, characterData: true })
|
||||
}
|
||||
|
||||
export function scrollIntoView(top: number) {
|
||||
el.scrollTo({ top, behavior: 'smooth' })
|
||||
}
|
||||
onMount(() => {
|
||||
observeScrollability(el)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
mutationObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class={twMerge('relative pb-1', scrollableClass)}>
|
||||
<div bind:this={el} on:scroll={handleScroll} class="w-full h-full overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
{#if !isAtBottom && isScrollable}
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-0 {shiftedShadow
|
||||
? 'left-2'
|
||||
: 'right-0'} h-14 w-full bg-gradient-to-t from-surface to-transparent"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { WindmillIcon2 } from './icons'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { Building } from 'lucide-svelte'
|
||||
export let selected: 'all' | 'hub' | 'workspace' = 'all'
|
||||
</script>
|
||||
|
||||
<div class="max-w-min">
|
||||
<ToggleButtonGroup bind:selected>
|
||||
<ToggleButton value="all" label="All" light small />
|
||||
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small />
|
||||
<ToggleButton value="workspace" icon={Building} label="Workspace" light small />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
@@ -54,7 +54,8 @@
|
||||
'bottom-start': 'origin-top-left left-0',
|
||||
'bottom-end': 'origin-top-right right-0',
|
||||
'top-start': 'origin-bottom-left left-0 bottom-0',
|
||||
'top-end': 'origin-bottom-right right-0 bottom-0'
|
||||
'top-end': 'origin-bottom-right right-0 bottom-0',
|
||||
'top-center': 'origin-top-left -top-full left-1/2 transform -translate-x-1/2 -translate-y-full'
|
||||
}
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
}
|
||||
|
||||
export let containerClasses: string = 'rounded-lg shadow-md border p-4 bg-surface'
|
||||
|
||||
export let floatingClasses: string = ''
|
||||
const [floatingRef, floatingContent] = createFloatingActions(floatingConfig)
|
||||
|
||||
export let blockOpen = false
|
||||
export let shouldUsePortal: boolean = true
|
||||
export let target: string | HTMLElement | undefined = undefined
|
||||
export let noTransition = false
|
||||
</script>
|
||||
|
||||
<Popover on:close class="leading-none">
|
||||
@@ -24,22 +25,30 @@
|
||||
</div>
|
||||
</PopoverButton>
|
||||
<ConditionalPortal condition={shouldUsePortal} {target}>
|
||||
<div use:floatingContent class="z5000">
|
||||
<Transition
|
||||
show={blockOpen || undefined}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel let:close static={blockOpen}>
|
||||
<div use:floatingContent class={`z5000 ${floatingClasses}`}>
|
||||
{#if !noTransition}
|
||||
<Transition
|
||||
show={blockOpen || undefined}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel let:close static={blockOpen}>
|
||||
<div class={containerClasses}>
|
||||
<slot {close} />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
{:else}
|
||||
<PopoverPanel focus={false} let:close static={blockOpen}>
|
||||
<div class={containerClasses}>
|
||||
<slot {close} />
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
{/if}
|
||||
</div>
|
||||
</ConditionalPortal>
|
||||
</Popover>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import { createFloatingActions, type ComputeConfig } from 'svelte-floating-ui'
|
||||
|
||||
export let floatingConfig: ComputeConfig = {
|
||||
strategy: 'absolute',
|
||||
//@ts-ignore
|
||||
placement: 'bottom-center'
|
||||
}
|
||||
export let open = false
|
||||
export let target: string | undefined = undefined
|
||||
|
||||
// export let containerClasses: string = 'rounded-lg shadow-md border p-4 bg-surface'
|
||||
// export let floatingClasses: string = ''
|
||||
const [floatingRef, floatingContent] = createFloatingActions(floatingConfig)
|
||||
|
||||
function close(div: Element | null) {
|
||||
open = false
|
||||
}
|
||||
|
||||
let acceptClickoutside = false
|
||||
function pointerup() {
|
||||
setTimeout(() => {
|
||||
acceptClickoutside = true
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function pointerdown() {
|
||||
if (acceptClickoutside && open) {
|
||||
open = false
|
||||
} else {
|
||||
acceptClickoutside = false
|
||||
open = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div use:floatingRef>
|
||||
<slot {pointerup} {pointerdown} name="button" />
|
||||
</div>
|
||||
|
||||
<Portal {target}>
|
||||
{#if open}
|
||||
<div
|
||||
class="border rounded-lg shadow-lg bg-surface z5000"
|
||||
style="position:absolute"
|
||||
use:floatingContent
|
||||
>
|
||||
<div
|
||||
use:clickOutside
|
||||
on:click_outside={() => {
|
||||
if (acceptClickoutside) {
|
||||
acceptClickoutside = false
|
||||
open = false
|
||||
}
|
||||
}}
|
||||
>
|
||||
<slot {close} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Portal>
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
async function onGenerate() {
|
||||
if (funcDesc.length <= 0) {
|
||||
if (funcDesc?.length <= 0) {
|
||||
return
|
||||
}
|
||||
savePrompt()
|
||||
@@ -122,7 +122,7 @@
|
||||
bind:this={input}
|
||||
bind:value={funcDesc}
|
||||
on:keypress={({ key }) => {
|
||||
if (key === 'Enter' && funcDesc.length > 0) {
|
||||
if (key === 'Enter' && funcDesc?.length > 0) {
|
||||
close(input || null)
|
||||
onGenerate()
|
||||
}
|
||||
@@ -139,7 +139,7 @@
|
||||
close(input || null)
|
||||
onGenerate()
|
||||
}}
|
||||
disabled={funcDesc.length <= 0}
|
||||
disabled={funcDesc?.length <= 0}
|
||||
iconOnly
|
||||
startIcon={{ icon: Wand2 }}
|
||||
/>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
f={(x) => (emptyString(x.summary) ? x.path : x.summary + ' (' + x.path + ')')}
|
||||
/>
|
||||
|
||||
<div class="text-primary transition-all {funcDesc.length > 0 ? 'w-96' : 'w-60'}">
|
||||
<div class="text-primary transition-all {funcDesc?.length > 0 ? 'w-96' : 'w-60'}">
|
||||
<div>
|
||||
<div class="flex p-2 relative">
|
||||
<input
|
||||
@@ -118,7 +118,7 @@
|
||||
bind:this={input}
|
||||
bind:value={funcDesc}
|
||||
on:input={() => {
|
||||
if (funcDesc.length > 2) {
|
||||
if (funcDesc?.length > 2) {
|
||||
getHubCompletions(funcDesc)
|
||||
} else {
|
||||
hubCompletions = []
|
||||
@@ -126,14 +126,14 @@
|
||||
}}
|
||||
placeholder="Search {trigger ? 'triggers' : 'scripts'} or AI gen"
|
||||
/>
|
||||
{#if funcDesc.length === 0}
|
||||
{#if funcDesc?.length === 0}
|
||||
<Wand2
|
||||
size={14}
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 fill-current opacity-70 text-violet-800 dark:text-violet-400"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !disableAi && funcDesc.length > 0}
|
||||
{#if !disableAi && funcDesc?.length > 0}
|
||||
<ul class="transition-all divide-y">
|
||||
<li>
|
||||
<button
|
||||
@@ -181,11 +181,11 @@
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{#if funcDesc.length > 0 && filteredItems.length > 0}
|
||||
{#if funcDesc?.length > 0 && filteredItems?.length > 0}
|
||||
<div class="text-left mt-2">
|
||||
<p class="text-xs text-secondary ml-2">Workspace {trigger ? 'Triggers' : 'Scripts'}</p>
|
||||
<ul class="transition-all divide-y">
|
||||
{#each filteredItems.slice(0, 3) as item (item.path)}
|
||||
{#each filteredItems?.slice(0, 3) ?? [] as item (item.path)}
|
||||
<li>
|
||||
<button
|
||||
class="py-2 gap-4 flex flex-row hover:bg-surface-hover transition-all items-center justify-between w-full"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { ScriptService, type Script } from '$lib/gen'
|
||||
|
||||
import { Wand2, Loader2 } from 'lucide-svelte'
|
||||
import SearchItems from '../SearchItems.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let funcDesc: string
|
||||
export let trigger = false
|
||||
export let loading = false
|
||||
export let preFilter: string
|
||||
|
||||
let scripts: Script[] | undefined = undefined
|
||||
export let filteredItems: (Script & { marked?: string })[] | (Item & { marked?: string })[] = []
|
||||
$: prefilteredItems = scripts ?? []
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
perPage: 300,
|
||||
kinds: trigger ? 'trigger' : 'script'
|
||||
})
|
||||
|
||||
scripts = loadedScripts
|
||||
}
|
||||
|
||||
$: scripts == undefined && funcDesc?.length > 1 && loadScripts()
|
||||
|
||||
let input: HTMLInputElement
|
||||
|
||||
$: preFilter &&
|
||||
setTimeout(() => {
|
||||
input?.focus()
|
||||
}, 50)
|
||||
|
||||
onMount(() => {
|
||||
input?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
filter={funcDesc}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (emptyString(x.summary) ? x.path : x.summary + ' (' + x.path + ')')}
|
||||
/>
|
||||
|
||||
<div class="relative text-primary items-center transition-all flex-grow">
|
||||
<div class="grow items-cente">
|
||||
<input
|
||||
bind:this={input}
|
||||
type="text"
|
||||
bind:value={funcDesc}
|
||||
placeholder="Search {trigger ? 'triggers' : 'scripts'} or AI gen"
|
||||
/>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none">
|
||||
{#if loading}
|
||||
<Loader2 size={16} class="animate-spin text-gray-400" />
|
||||
{/if}
|
||||
{#if funcDesc?.length === 0 && !loading}
|
||||
<Wand2 size={14} class="fill-current opacity-70 text-violet-800 dark:text-violet-400" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +14,7 @@ import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
|
||||
export type FlowCopilotModule = {
|
||||
id: string
|
||||
type: 'trigger' | 'script'
|
||||
type: 'trigger' | 'script'
|
||||
description: string
|
||||
code: string
|
||||
source: 'hub' | 'custom' | undefined
|
||||
@@ -25,6 +25,8 @@ export type FlowCopilotModule = {
|
||||
kind: string
|
||||
app: string
|
||||
ask_id: number
|
||||
id: number
|
||||
version_id: number
|
||||
}[]
|
||||
selectedCompletion:
|
||||
| {
|
||||
@@ -104,6 +106,69 @@ To maintain state across runs, you can use get_state() and set_state(value) whic
|
||||
{additionalInformation}`
|
||||
}
|
||||
|
||||
|
||||
// const preprocessorPrompts: {
|
||||
// bun: string
|
||||
// python3: string
|
||||
// } = {
|
||||
// bun: `I'm building a workflow which is a sequence of script steps. Write the preprocessor step in {codeLang} which should check for {description} and return an array.
|
||||
// The preprocessor step is executed before flow begins to map trigger specific inputs to the flow inputs.
|
||||
|
||||
// Here is an example of what the preprocessor step should look like:
|
||||
// \`\`\`{codeLang}
|
||||
// export async function preprocessor(
|
||||
// wm_trigger: {
|
||||
// kind: 'http' | 'email' | 'webhook',
|
||||
// http?: {
|
||||
// route: string // The route path, e.g. "/users/:id"
|
||||
// path: string // The actual path called, e.g. "/users/123"
|
||||
// method: string
|
||||
// params: Record<string, string>
|
||||
// query: Record<string, string>
|
||||
// headers: Record<string, string>
|
||||
// }
|
||||
// },
|
||||
// /* your other args */
|
||||
// ) {
|
||||
// return {
|
||||
// // return the args to be passed to the flow
|
||||
// }
|
||||
// }
|
||||
// \`\`\`
|
||||
|
||||
// {additionalInformation}`,
|
||||
// python3: `I'm building a workflow which is a sequence of script steps. Write the preprocessor step in {codeLang} which should check for {description} and return an array.
|
||||
// The preprocessor step is executed before flow begins to map trigger specific inputs to the flow inputs.
|
||||
|
||||
// Here is an example of what the preprocessor step should look like:
|
||||
// \`\`\`{codeLang}
|
||||
// from typing import TypedDict, Literal
|
||||
|
||||
// class Http(TypedDict):
|
||||
// route: str # The route path, e.g. "/users/:id"
|
||||
// path: str # The actual path called, e.g. "/users/123"
|
||||
// method: str
|
||||
// params: dict[str, str]
|
||||
// query: dict[str, str]
|
||||
// headers: dict[str, str]
|
||||
|
||||
// class WmTrigger(TypedDict):
|
||||
// kind: Literal["http", "email", "webhook"]
|
||||
// http: Http | None
|
||||
|
||||
// def preprocessor(
|
||||
// wm_trigger: WmTrigger,
|
||||
// # your other args
|
||||
// ):
|
||||
// return {
|
||||
// # return the args to be passed to the flow
|
||||
// }
|
||||
// \`\`\`
|
||||
|
||||
// {additionalInformation}`
|
||||
// }
|
||||
|
||||
|
||||
const firstActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}.
|
||||
Return the script's output.
|
||||
|
||||
@@ -200,6 +265,8 @@ export async function stepCopilot(
|
||||
let prompt =
|
||||
module.type === 'trigger'
|
||||
? triggerPrompts[lang]
|
||||
// : module.type === 'preprocessor'
|
||||
// ? preprocessorPrompts[lang]
|
||||
: pastModule === undefined
|
||||
? firstActionPrompt
|
||||
: isFirstInLoop
|
||||
|
||||
@@ -18,13 +18,14 @@
|
||||
export let disableSettings = false
|
||||
export let smallErrorHandler = false
|
||||
|
||||
let size = 40
|
||||
let size = 50
|
||||
|
||||
const { currentStepStore: copilotCurrentStepStore } =
|
||||
getContext<FlowCopilotContext>('FlowCopilotContext')
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="flow-editor"
|
||||
class={classNames(
|
||||
'h-full overflow-hidden transition-colors duration-[400ms] ease-linear border-t',
|
||||
$copilotCurrentStepStore !== undefined ? 'border-gray-500/75' : ''
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import { FlowService, type Flow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
// export let failureModule: boolean
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let items: Flow[] | undefined = undefined
|
||||
let filteredItems: (Flow & { marked?: string })[] | undefined = undefined
|
||||
export let filter = ''
|
||||
$: $workspaceStore && loadFlows()
|
||||
|
||||
let ownerFilter: string | undefined = undefined
|
||||
$: prefilteredItems = ownerFilter ? items?.filter((x) => x.path.startsWith(ownerFilter!)) : items
|
||||
|
||||
export let owners: string[] = []
|
||||
$: owners = Array.from(
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
|
||||
async function loadFlows() {
|
||||
items = await FlowService.listFlows({ workspace: $workspaceStore! })
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={prefilteredItems}
|
||||
bind:filteredItems
|
||||
f={(x) => (emptyString(x.summary) ? x.path : x.summary + ' (' + x.path + ')')}
|
||||
/>
|
||||
<div class="flex flex-col min-h-0">
|
||||
{#if filteredItems}
|
||||
{#if filter.length > 0 && filteredItems.length == 0}
|
||||
<div class="text-2xs text-tercary font-extralight text-center py-2 px-3 items-center">
|
||||
No items found.
|
||||
</div>
|
||||
{/if}
|
||||
<ul class="overflow-auto">
|
||||
{#each filteredItems as { path, summary, marked }}
|
||||
<li class="flex flex-row w-full">
|
||||
<button
|
||||
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover bg-surface transition-all items-center rounded-md text-left text-2xs text-primary font-normal"
|
||||
on:click={async () => {
|
||||
dispatch('pickFlow', {
|
||||
path,
|
||||
summary
|
||||
})
|
||||
}}
|
||||
>
|
||||
<BarsStaggered size={14} />
|
||||
<span class="grow truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[0.5, [1.5]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,455 @@
|
||||
<script lang="ts">
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import FlowScriptPickerQuick from '../pickers/FlowScriptPickerQuick.svelte'
|
||||
import WorkspaceScriptPickerQuick from '../pickers/WorkspaceScriptPickerQuick.svelte'
|
||||
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
|
||||
import { defaultScripts, enterpriseLicense, userStore } from '$lib/stores'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
||||
import PickHubScriptQuick from '../pickers/PickHubScriptQuick.svelte'
|
||||
import { type Script, type FlowModule } from '$lib/gen'
|
||||
import ListFiltersQuick from '$lib/components/home/ListFiltersQuick.svelte'
|
||||
import { Folder, User } from 'lucide-svelte'
|
||||
import type { FlowCopilotContext, FlowCopilotModule } from '../../copilot/flow'
|
||||
import type { FlowEditorContext } from '../../flows/types'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
import { nextId } from '../../flows/flowModuleNextId'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { flip } from 'svelte/animate'
|
||||
import Scrollable from '$lib/components/Scrollable.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { SettingsIcon } from 'lucide-svelte'
|
||||
import DefaultScriptsInner from '$lib/components/DefaultScriptsInner.svelte'
|
||||
import GenAiQuick from './GenAiQuick.svelte'
|
||||
import FlowToplevelNode from '../pickers/FlowToplevelNode.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let summary: string | undefined = undefined
|
||||
export let filter = ''
|
||||
export let disableAi = false
|
||||
export let preFilter: 'all' | 'workspace' | 'hub' = 'hub'
|
||||
export let funcDesc: string
|
||||
export let index: number
|
||||
export let modules: FlowModule[]
|
||||
export let owners: string[] = []
|
||||
export let loading = false
|
||||
export let small = false
|
||||
export let kind: 'trigger' | 'script' | 'preprocessor' | 'failure' | 'approval'
|
||||
export let selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure' =
|
||||
kind
|
||||
|
||||
let lang: FlowCopilotModule['lang'] = undefined
|
||||
let selectedCompletion: FlowCopilotModule['selectedCompletion'] = undefined
|
||||
|
||||
let filteredWorkspaceItems: (Script & { marked?: string })[] = []
|
||||
|
||||
let hubCompletions: FlowCopilotModule['hubCompletions'] = []
|
||||
|
||||
const { flowStore, flowStateStore, insertButtonOpen } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { modulesStore: copilotModulesStore, genFlow } =
|
||||
getContext<FlowCopilotContext>('FlowCopilotContext')
|
||||
|
||||
let selected: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined = undefined
|
||||
|
||||
let integrations: string[] = []
|
||||
|
||||
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
|
||||
|
||||
$: langs = processLangs(undefined, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
|
||||
.map((l) => [defaultScriptLanguages[l], l])
|
||||
.filter(
|
||||
(x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1])
|
||||
) as [string, SupportedLanguage | 'docker'][]
|
||||
|
||||
function displayLang(
|
||||
lang: SupportedLanguage | 'docker',
|
||||
kind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
|
||||
) {
|
||||
if (kind == 'trigger') {
|
||||
return ['python3', 'bun', 'deno', 'go'].includes(lang)
|
||||
} else if (kind == 'script') {
|
||||
return true
|
||||
} else if (kind == 'approval') {
|
||||
return ['python3', 'bun', 'deno'].includes(lang)
|
||||
} else if (kind == 'flow') {
|
||||
return false
|
||||
} else if (kind == 'preprocessor') {
|
||||
return ['python3', 'bun', 'deno'].includes(lang)
|
||||
} else if (kind == 'failure') {
|
||||
return ['python3', 'bun', 'deno', 'go'].includes(lang)
|
||||
}
|
||||
}
|
||||
|
||||
async function onGenerate() {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_openai_resource_path) {
|
||||
sendUserToast(
|
||||
'Windmill AI is not enabled, you can activate it in the workspace settings',
|
||||
true
|
||||
)
|
||||
return
|
||||
}
|
||||
$copilotModulesStore = [
|
||||
{
|
||||
id: nextId($flowStateStore, $flowStore),
|
||||
type: selectedKind == 'trigger' ? 'trigger' : 'script',
|
||||
description: funcDesc,
|
||||
code: '',
|
||||
source: selectedCompletion ? 'hub' : 'custom',
|
||||
hubCompletions,
|
||||
selectedCompletion,
|
||||
editor: undefined,
|
||||
lang
|
||||
}
|
||||
]
|
||||
genFlow?.(index, modules, true)
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
let openScriptSettings = false
|
||||
|
||||
let selectedByKeyboard = 0
|
||||
|
||||
$: onSelectedKindChange(selectedKind)
|
||||
|
||||
function onSelectedKindChange(
|
||||
_selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
|
||||
) {
|
||||
selectedByKeyboard = 0
|
||||
}
|
||||
|
||||
let inlineScripts: [string, SupportedLanguage | 'docker'][] = []
|
||||
|
||||
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql']
|
||||
|
||||
function computeInlineScriptChoices(
|
||||
funcDesc: string,
|
||||
selected: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined,
|
||||
preFilter: 'all' | 'workspace' | 'hub',
|
||||
selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
|
||||
) {
|
||||
if (['script', 'trigger', 'failure', 'approval', 'preprocessor'].includes(selectedKind)) {
|
||||
if (!selected && preFilter == 'all') {
|
||||
inlineScripts = langs.filter((lang) => {
|
||||
return (
|
||||
(customUi?.languages == undefined || customUi?.languages?.includes(lang?.[1])) &&
|
||||
(funcDesc?.length == 0 ||
|
||||
lang?.[0]?.toLowerCase()?.includes(funcDesc?.toLowerCase())) &&
|
||||
displayLang(lang?.[1], selectedKind)
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
inlineScripts = []
|
||||
}
|
||||
|
||||
const allToplevelNodes: [string, string][] = [
|
||||
['For Loop', 'forloop'],
|
||||
['While Loop', 'whileloop'],
|
||||
['Branch to one', 'branchone'],
|
||||
['Branch to all', 'branchall']
|
||||
]
|
||||
let topLevelNodes: [string, string][] = []
|
||||
function computeToplevelNodeChoices(funcDesc: string, preFilter: 'all' | 'workspace' | 'hub') {
|
||||
if (funcDesc.length > 0 && preFilter == 'all' && kind == 'script') {
|
||||
topLevelNodes = allToplevelNodes.filter((node) =>
|
||||
node[0].toLowerCase().startsWith(funcDesc.toLowerCase())
|
||||
)
|
||||
} else {
|
||||
topLevelNodes = []
|
||||
}
|
||||
}
|
||||
|
||||
$: computeToplevelNodeChoices(funcDesc, preFilter)
|
||||
$: computeInlineScriptChoices(funcDesc, selected, preFilter, selectedKind)
|
||||
$: onPrefilterChange(preFilter)
|
||||
|
||||
function onPrefilterChange(preFilter: 'all' | 'workspace' | 'hub') {
|
||||
if (preFilter == 'workspace') {
|
||||
hubCompletions = []
|
||||
} else if (preFilter == 'hub') {
|
||||
filteredWorkspaceItems = []
|
||||
}
|
||||
selectedByKeyboard = 0
|
||||
}
|
||||
|
||||
$: aiLength =
|
||||
funcDesc?.length > 0 && !disableAi && selectedKind != 'flow' && preFilter == 'all' ? 2 : 0
|
||||
|
||||
let scrollable: Scrollable | undefined
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
let length =
|
||||
topLevelNodes?.length +
|
||||
inlineScripts.length +
|
||||
aiLength +
|
||||
filteredWorkspaceItems.length +
|
||||
hubCompletions.length
|
||||
if (e.key === 'ArrowDown') {
|
||||
selectedByKeyboard = (selectedByKeyboard + 1) % length
|
||||
scrollable?.scrollIntoView(selectedByKeyboard * 32)
|
||||
e.preventDefault()
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
selectedByKeyboard = (selectedByKeyboard - 1 + length) % length
|
||||
scrollable?.scrollIntoView(selectedByKeyboard * 32)
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
$insertButtonOpen = true
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
$insertButtonOpen = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
<div class="flex flex-row grow min-w-0 divide-x relative {!small ? 'shadow-inset' : ''}">
|
||||
{#if selectedKind != 'preprocessor'}
|
||||
<Scrollable shiftedShadow scrollableClass="w-32 grow-0 shrink-0 ">
|
||||
{#if ['script', 'trigger', 'approval', 'preprocessor', 'failure'].includes(selectedKind)}
|
||||
{#if (preFilter === 'all' && owners.length > 0) || preFilter === 'workspace'}
|
||||
{#if preFilter !== 'workspace'}
|
||||
<div class="pb-0 text-2xs font-light text-secondary ml-2">Workspace Folders</div>
|
||||
{/if}
|
||||
|
||||
{#if owners.length > 0}
|
||||
{#each owners as owner (owner)}
|
||||
<div
|
||||
in:fade={{ duration: 50 }}
|
||||
animate:flip={{ duration: 100 }}
|
||||
class="w-full px-0.5 pb-1.5"
|
||||
>
|
||||
<button
|
||||
class={twMerge(
|
||||
'w-full text-left text-2xs text-primary font-normal py-2 px-3 hover:bg-surface-hover transition-all whitespace-nowrap flex flex-row gap-2 items-center rounded-md',
|
||||
owner === selected?.name ? 'bg-surface-hover' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
selected = selected?.name == owner ? undefined : { kind: 'owner', name: owner }
|
||||
}}
|
||||
>
|
||||
{#if owner.startsWith('f/')}
|
||||
<Folder class="mr-0.5" size={14} />
|
||||
{:else}
|
||||
<User class="mr-0.5" size={14} />
|
||||
{/if}
|
||||
{owner.slice(2)}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-2xs text-tertiary font-light text-center py-3 px-3 items-center">
|
||||
No items found.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if preFilter === 'hub' || preFilter === 'all'}
|
||||
{#if preFilter == 'all'}
|
||||
<div class="pb-0 text-2xs font-light text-secondary ml-2 pt-0.5">Integrations</div>
|
||||
{/if}
|
||||
<ListFiltersQuick filters={integrations} bind:selectedFilter={selected} resourceType />
|
||||
{/if}
|
||||
{:else if selectedKind === 'flow'}
|
||||
{#if owners.length > 0}
|
||||
{#each owners as owner (owner)}
|
||||
<div in:fade={{ duration: 50 }} animate:flip={{ duration: 100 }}>
|
||||
<button
|
||||
class={twMerge(
|
||||
'w-full text-left text-2xs text-primary font-normal py-2 px-3 hover:bg-surface-hover transition-all whitespace-nowrap flex flex-row gap-2 items-center rounded-md',
|
||||
owner === selected?.name ? 'bg-surface-hover' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
selected = selected?.name == owner ? undefined : { kind: 'owner', name: owner }
|
||||
}}
|
||||
>
|
||||
{#if owner.startsWith('f/')}
|
||||
<Folder class="mr-0.5" size={14} />
|
||||
{:else}
|
||||
<User class="mr-0.5" size={14} />
|
||||
{/if}
|
||||
{owner.slice(2)}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</Scrollable>
|
||||
{/if}
|
||||
<Scrollable bind:this={scrollable} scrollableClass="grow min-w-0">
|
||||
{#if kind == 'script'}
|
||||
{#each topLevelNodes as [label, kind], i (label)}
|
||||
<FlowToplevelNode
|
||||
on:click={() => {
|
||||
dispatch('new', { kind })
|
||||
}}
|
||||
{label}
|
||||
selected={selectedByKeyboard === i}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if inlineScripts?.length > 0}
|
||||
<div class="pb-0 flex flex-row items-center gap-2">
|
||||
<div class=" text-2xs font-light text-secondary ml-2"
|
||||
>New {selectedKind != 'script' ? selectedKind + ' ' : ''}script</div
|
||||
>
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
{#if !openScriptSettings}
|
||||
<Button
|
||||
on:click={() => (openScriptSettings = true)}
|
||||
startIcon={{ icon: SettingsIcon }}
|
||||
color="light"
|
||||
size="xs2"
|
||||
btnClasses="!text-tertiary"
|
||||
variant="contained"
|
||||
title="Edit global default scripts"
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
on:click={() => (openScriptSettings = false)}
|
||||
startIcon={{ icon: SettingsIcon }}
|
||||
color="dark"
|
||||
size="xs2"
|
||||
variant="contained"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if openScriptSettings}
|
||||
<div class="p-2">
|
||||
<DefaultScriptsInner small />
|
||||
</div>
|
||||
{/if}
|
||||
{#each inlineScripts as [label, lang], i (lang)}
|
||||
<FlowScriptPickerQuick
|
||||
eeRestricted={!$enterpriseLicense && enterpriseLangs.includes(lang)}
|
||||
selected={selectedByKeyboard === i + topLevelNodes.length}
|
||||
{enterpriseLangs}
|
||||
{label}
|
||||
lang={lang == 'docker' ? 'bash' : lang}
|
||||
on:click={() => {
|
||||
if (lang == 'docker') {
|
||||
if (isCloudHosted()) {
|
||||
sendUserToast(
|
||||
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
|
||||
true,
|
||||
[
|
||||
{
|
||||
label: 'Learn more',
|
||||
callback: () => {
|
||||
window.open('https://www.windmill.dev/docs/advanced/docker', '_blank')
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('new', {
|
||||
kind: 'script',
|
||||
inlineScript: {
|
||||
language: lang == 'docker' ? 'bash' : lang,
|
||||
kind: selectedKind,
|
||||
subkind:
|
||||
lang == 'docker'
|
||||
? 'docker'
|
||||
: selectedKind == 'preprocessor'
|
||||
? 'preprocessor'
|
||||
: 'flow',
|
||||
summary
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if !disableAi && funcDesc?.length > 0 && kind != 'failure' && kind != 'preprocessor' && (selectedKind == 'script' || selectedKind == 'trigger') && preFilter == 'all'}
|
||||
<ul class="transition-all">
|
||||
<li
|
||||
><GenAiQuick
|
||||
{funcDesc}
|
||||
lang="TypeScript"
|
||||
selected={selectedByKeyboard === inlineScripts?.length + topLevelNodes.length}
|
||||
on:click={() => {
|
||||
lang = 'bun'
|
||||
onGenerate()
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<GenAiQuick
|
||||
{funcDesc}
|
||||
lang="Python"
|
||||
selected={selectedByKeyboard === inlineScripts?.length + topLevelNodes.length + 1}
|
||||
on:click={() => {
|
||||
lang = 'python3'
|
||||
onGenerate()
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if (!selected || selected?.kind === 'owner') && (preFilter === 'workspace' || preFilter === 'all')}
|
||||
{#if !selected && (preFilter !== 'workspace' || funcDesc?.length > 0)}
|
||||
<div class="pt-2 pb-0 text-2xs font-light text-secondary ml-2">Workspace</div>
|
||||
{/if}
|
||||
<WorkspaceScriptPickerQuick
|
||||
bind:owners
|
||||
bind:ownerFilter={selected}
|
||||
bind:filteredWithOwner={filteredWorkspaceItems}
|
||||
{filter}
|
||||
kind={selectedKind}
|
||||
selected={selectedByKeyboard - inlineScripts?.length - aiLength - topLevelNodes.length}
|
||||
on:pickScript
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if selectedKind != 'preprocessor' && selectedKind != 'flow'}
|
||||
{#if (!selected || selected?.kind === 'integrations') && (preFilter === 'hub' || preFilter === 'all')}
|
||||
{#if !selected && preFilter !== 'hub'}
|
||||
<div class=" pb-0 text-2xs font-light text-secondary ml-2">Hub</div>
|
||||
{/if}
|
||||
<PickHubScriptQuick
|
||||
bind:items={hubCompletions}
|
||||
bind:filter
|
||||
bind:apps={integrations}
|
||||
appFilter={selected?.name}
|
||||
kind={selectedKind}
|
||||
selected={selectedByKeyboard -
|
||||
inlineScripts?.length -
|
||||
aiLength -
|
||||
filteredWorkspaceItems?.length -
|
||||
topLevelNodes.length}
|
||||
on:pickScript
|
||||
bind:loading
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</Scrollable>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.shadow-inset::before {
|
||||
box-shadow: inset 25px 0px 12px -30px rgba(94, 129, 172, 0.5);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Wand2 } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let funcDesc: string
|
||||
export let selected: boolean
|
||||
export let lang: string
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (selected && e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
dispatch('click')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
<button
|
||||
class="px-3 py-2 gap-2 w-full text-left hover:bg-surface-hover flex flex-row items-center transition-all rounded-md {selected
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click
|
||||
>
|
||||
<Wand2 size={14} class="text-violet-800 dark:text-violet-400" />
|
||||
|
||||
<span class="grow truncate text-left text-2xs text-primary font-normal">
|
||||
Generate "{funcDesc}" in {lang}
|
||||
</span>
|
||||
{#if selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -3,37 +3,52 @@
|
||||
import { getContext } from 'svelte'
|
||||
import { classNames, emptySchema } from '$lib/utils'
|
||||
import type { FlowModuleState } from '../flowState'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { NEVER_TESTED_THIS_FAR } from '../models'
|
||||
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Bug } from 'lucide-svelte'
|
||||
import { Bug, X } from 'lucide-svelte'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
import { createInlineScriptModule, pickScript } from '$lib/components/flows/flowStateUtils'
|
||||
import type { FlowModule, RawScript } from '$lib/gen'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let small: boolean
|
||||
|
||||
const { selectedId, flowStateStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
function onToggle() {
|
||||
if ($flowStore?.value?.failure_module) {
|
||||
$flowStore.value.failure_module = undefined
|
||||
// By default, we return to settings when disabling the failure module
|
||||
$selectedId = 'settings-metadata'
|
||||
} else {
|
||||
const failureModule: FlowModuleState = {
|
||||
schema: emptySchema(),
|
||||
previewResult: NEVER_TESTED_THIS_FAR
|
||||
}
|
||||
|
||||
$flowStore.value.failure_module = {
|
||||
id: 'failure',
|
||||
value: { type: 'identity' }
|
||||
}
|
||||
$flowStateStore['failure'] = failureModule
|
||||
|
||||
$selectedId = 'failure'
|
||||
$flowStore = $flowStore
|
||||
async function insertNewFailureModule(
|
||||
inlineScript?: {
|
||||
language: RawScript['language']
|
||||
subkind: 'pgsql' | 'flow'
|
||||
},
|
||||
wsScript?: { path: string; summary: string; hash: string | undefined }
|
||||
) {
|
||||
var module: FlowModule = {
|
||||
id: 'failure',
|
||||
value: { type: 'identity' }
|
||||
}
|
||||
var state: FlowModuleState = {
|
||||
schema: emptySchema(),
|
||||
previewResult: NEVER_TESTED_THIS_FAR
|
||||
}
|
||||
|
||||
if (inlineScript) {
|
||||
;[module, state] = await createInlineScriptModule(
|
||||
inlineScript.language,
|
||||
'failure',
|
||||
inlineScript.subkind,
|
||||
'failure'
|
||||
)
|
||||
} else if (wsScript) {
|
||||
;[module, state] = await pickScript(wsScript.path, wsScript.summary, module.id, wsScript.hash)
|
||||
}
|
||||
|
||||
$flowStore.value.failure_module = module
|
||||
$flowStateStore[module.id] = state
|
||||
|
||||
$selectedId = 'failure'
|
||||
$flowStore = $flowStore
|
||||
}
|
||||
|
||||
const { currentStepStore: copilotCurrentStepStore } =
|
||||
@@ -42,45 +57,71 @@
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
'z-10',
|
||||
$copilotCurrentStepStore !== undefined ? 'border-gray-500/75' : 'cursor-pointer',
|
||||
'border transition-colors duration-[400ms] ease-linear rounded-sm px-2 py-1 gap-2 bg-surface text-sm flex items-center flex-row',
|
||||
$selectedId?.includes('failure')
|
||||
? 'outline outline-offset-1 outline-2 outline-slate-900 dark:outline-slate-900/0 dark:bg-surface-secondary dark:border-gray-400'
|
||||
: ''
|
||||
)}
|
||||
style="min-width: {small ? '200px' : '230px'}; max-width: 275px;"
|
||||
on:click={() => {
|
||||
if ($copilotCurrentStepStore !== undefined) return
|
||||
if ($flowStore?.value?.failure_module) {
|
||||
$selectedId = 'failure'
|
||||
} else {
|
||||
onToggle()
|
||||
}
|
||||
}}
|
||||
class={classNames(
|
||||
'z-10',
|
||||
$copilotCurrentStepStore !== undefined ? 'border-gray-500/75' : 'cursor-pointer',
|
||||
'border transition-colors duration-[400ms] ease-linear rounded-sm px-2 py-1 bg-surface text-sm flex justify-between items-center flex-row overflow-x-hidden relative',
|
||||
$selectedId?.includes('failure') ? 'outline outline-offset-1 outline-2 outline-slate-900 dark:outline-slate-900/0 dark:bg-surface-secondary dark:border-gray-400' : ''
|
||||
)}
|
||||
style={small ? 'min-width: 200px' : 'min-width: 275px'}
|
||||
>
|
||||
{#if $copilotCurrentStepStore !== undefined}
|
||||
<div transition:fade class="absolute inset-0 bg-gray-500 bg-opacity-75 z-[900]" />
|
||||
{/if}
|
||||
<div class=" flex justify-between items-center flex-wrap gap-2">
|
||||
<Bug size={16} />
|
||||
<span class="font-bold text-xs">Error Handler</span>
|
||||
<div class="flex items-center grow-0 min-w-0 gap-2">
|
||||
<Bug size={16} color={$flowStore?.value?.failure_module ? '#3b82f6' : '#9CA3AF'} />
|
||||
</div>
|
||||
|
||||
<div class=" items-center truncate flex text-xs">
|
||||
{#if Boolean($flowStore?.value?.failure_module)}
|
||||
<span>
|
||||
{$flowStore.value.failure_module?.summary ||
|
||||
($flowStore.value.failure_module?.value.type === 'rawscript'
|
||||
? `${$flowStore.value.failure_module?.value.language}`
|
||||
: 'TBD')}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Toggle
|
||||
size={small ? 'xs' : 'sm'}
|
||||
checked={Boolean($flowStore?.value?.failure_module)}
|
||||
on:change={onToggle}
|
||||
id="error-handler-toggle"
|
||||
/>
|
||||
{#if !$flowStore?.value?.failure_module}
|
||||
<div class="grow text-center font-bold text-xs">Error Handler</div>
|
||||
{:else}
|
||||
<div class="truncate grow min-w-0 text-center text-xs">
|
||||
{$flowStore.value.failure_module?.summary ||
|
||||
($flowStore.value.failure_module?.value.type === 'rawscript'
|
||||
? `${$flowStore.value.failure_module?.value.language}`
|
||||
: 'TBD')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !$flowStore?.value?.failure_module}
|
||||
<InsertModuleButton
|
||||
disableAi={false}
|
||||
index={0}
|
||||
placement={'top-center'}
|
||||
on:new={(e) => {
|
||||
insertNewFailureModule(e.detail.inlineScript)
|
||||
}}
|
||||
on:pickScript={(e) => {
|
||||
insertNewFailureModule(undefined, e.detail)
|
||||
}}
|
||||
kind="failure"
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
title="Delete failure script"
|
||||
type="button"
|
||||
class={twMerge(
|
||||
'w-5 h-5 flex items-center justify-center grow-0 shrink-0',
|
||||
'outline-[1px] outline dark:outline-gray-500 outline-gray-300',
|
||||
'text-secondary',
|
||||
'bg-surface focus:outline-none hover:bg-surface-hover rounded '
|
||||
)}
|
||||
on:click={() => {
|
||||
$flowStore.value.failure_module = undefined
|
||||
$selectedId = 'settings-metadata'
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import {
|
||||
createInlineScriptModule,
|
||||
createBranchAll,
|
||||
createBranches,
|
||||
createLoop,
|
||||
createWhileLoop,
|
||||
deleteFlowStateById,
|
||||
emptyModule,
|
||||
pickScript
|
||||
pickScript,
|
||||
pickFlow
|
||||
} from '$lib/components/flows/flowStateUtils'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { FlowModule, RawScript, Script } from '$lib/gen'
|
||||
import { emptyFlowModuleState, initFlowStepWarnings } from '../utils'
|
||||
import FlowSettingsItem from './FlowSettingsItem.svelte'
|
||||
import FlowConstantsItem from './FlowConstantsItem.svelte'
|
||||
@@ -31,8 +33,6 @@
|
||||
import { tutorialInProgress } from '$lib/tutorialUtils'
|
||||
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
|
||||
import { replaceId } from '../flowStore'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { NEVER_TESTED_THIS_FAR } from '../models'
|
||||
|
||||
export let modules: FlowModule[] | undefined
|
||||
export let sidebarSize: number | undefined = undefined
|
||||
@@ -61,12 +61,22 @@
|
||||
| 'trigger'
|
||||
| 'approval'
|
||||
| 'end',
|
||||
wsScript?: { path: string; summary: string; hash: string | undefined }
|
||||
wsScript?: { path: string; summary: string; hash: string | undefined },
|
||||
wsFlow?: { path: string; summary: string },
|
||||
inlineScript?: {
|
||||
language: RawScript['language']
|
||||
kind: Script['kind']
|
||||
subkind: 'pgsql' | 'flow'
|
||||
id: string
|
||||
summary?: string
|
||||
}
|
||||
): Promise<FlowModule[]> {
|
||||
push(history, $flowStore)
|
||||
var module = emptyModule($flowStateStore, $flowStore, kind == 'flow')
|
||||
var state = emptyFlowModuleState()
|
||||
if (wsScript) {
|
||||
if (wsFlow) {
|
||||
;[module, state] = await pickFlow(wsFlow.path, wsFlow.summary, module.id)
|
||||
} else if (wsScript) {
|
||||
;[module, state] = await pickScript(wsScript.path, wsScript.summary, module.id, wsScript.hash)
|
||||
} else if (kind == 'forloop') {
|
||||
;[module, state] = await createLoop(
|
||||
@@ -89,11 +99,49 @@
|
||||
module.summary = 'Terminate flow'
|
||||
module.stop_after_if = { skip_if_stopped: false, expr: 'true' }
|
||||
}
|
||||
if (inlineScript) {
|
||||
const { language, kind, subkind } = inlineScript
|
||||
;[module, state] = await createInlineScriptModule(
|
||||
language,
|
||||
kind,
|
||||
subkind,
|
||||
module.id,
|
||||
module.summary
|
||||
)
|
||||
}
|
||||
if (!modules) return [module]
|
||||
modules.splice(index, 0, module)
|
||||
return modules
|
||||
}
|
||||
|
||||
async function insertNewPreprocessorModule(
|
||||
inlineScript?: {
|
||||
language: RawScript['language']
|
||||
subkind: 'pgsql' | 'flow'
|
||||
},
|
||||
wsScript?: { path: string; summary: string; hash: string | undefined }
|
||||
) {
|
||||
var module: FlowModule = {
|
||||
id: 'preprocessor',
|
||||
value: { type: 'identity' }
|
||||
}
|
||||
var state = emptyFlowModuleState()
|
||||
|
||||
if (inlineScript) {
|
||||
;[module, state] = await createInlineScriptModule(
|
||||
inlineScript.language,
|
||||
'script',
|
||||
inlineScript.subkind,
|
||||
'preprocessor'
|
||||
)
|
||||
} else if (wsScript) {
|
||||
;[module, state] = await pickScript(wsScript.path, wsScript.summary, module.id, wsScript.hash)
|
||||
}
|
||||
|
||||
$flowStore.value.preprocessor_module = module
|
||||
$flowStateStore[module.id] = state
|
||||
}
|
||||
|
||||
function removeAtId(modules: FlowModule[], id: string): FlowModule[] {
|
||||
const index = modules.findIndex((mod) => mod.id == id)
|
||||
if (index != -1) {
|
||||
@@ -311,22 +359,16 @@
|
||||
$moving = undefined
|
||||
} else {
|
||||
if (detail.detail === 'preprocessor') {
|
||||
const preprocessorModule = {
|
||||
schema: emptySchema(),
|
||||
previewResult: NEVER_TESTED_THIS_FAR
|
||||
}
|
||||
$flowStore.value.preprocessor_module = {
|
||||
id: 'preprocessor',
|
||||
value: { type: 'identity' }
|
||||
}
|
||||
$flowStateStore['preprocessor'] = preprocessorModule
|
||||
insertNewPreprocessorModule(detail.inlineScript, detail.script)
|
||||
$selectedId = 'preprocessor'
|
||||
} else {
|
||||
await insertNewModuleAtIndex(
|
||||
detail.modules,
|
||||
detail.index ?? 0,
|
||||
detail.detail,
|
||||
detail.script
|
||||
detail.kind,
|
||||
detail.script,
|
||||
detail.flow,
|
||||
detail.inlineScript
|
||||
)
|
||||
$selectedId = detail.modules[detail.index ?? 0].id
|
||||
}
|
||||
|
||||
@@ -1,169 +1,200 @@
|
||||
<script lang="ts">
|
||||
import { Menu } from '$lib/components/common'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { CheckCircle2, Code, Cross, GitBranch, Repeat, Square, Zap } from 'lucide-svelte'
|
||||
import StepGen from '$lib/components/copilot/StepGen.svelte'
|
||||
import { Cross, Zap } from 'lucide-svelte'
|
||||
import StepGenQuick from '$lib/components/copilot/StepGenQuick.svelte'
|
||||
import FlowInputsQuick from '../content/FlowInputsQuick.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
|
||||
import ToggleHubWorkspaceQuick from '$lib/components/ToggleHubWorkspaceQuick.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { ComputeConfig } from 'svelte-floating-ui'
|
||||
import TopLevelNode from '../pickers/TopLevelNode.svelte'
|
||||
import PopupV2 from '$lib/components/common/popup/PopupV2.svelte'
|
||||
import { flip, offset } from 'svelte-floating-ui/dom'
|
||||
|
||||
// import type { Writable } from 'svelte/store'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
export let trigger = false
|
||||
export let stop = false
|
||||
export let open: boolean | undefined = undefined
|
||||
export let index: number
|
||||
export let index: number = 0
|
||||
export let funcDesc = ''
|
||||
export let modules: FlowModule[]
|
||||
export let modules: FlowModule[] = []
|
||||
export let disableAi = false
|
||||
export let kind: 'script' | 'trigger' | 'preprocessor' | 'failure' = 'script'
|
||||
export let allowTrigger = true
|
||||
|
||||
type Alignment = 'start' | 'end' | 'center'
|
||||
type Side = 'top' | 'bottom'
|
||||
type Placement = `${Side}-${Alignment}`
|
||||
|
||||
export let placement: Placement = 'bottom-center'
|
||||
|
||||
let floatingConfig: ComputeConfig = {
|
||||
strategy: 'fixed',
|
||||
// @ts-ignore
|
||||
placement,
|
||||
middleware: [offset(8), flip()],
|
||||
autoUpdate: true
|
||||
}
|
||||
$: !open && (funcDesc = '')
|
||||
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
|
||||
let selectedKind: 'script' | 'trigger' | 'preprocessor' | 'approval' | 'flow' | 'failure' = kind
|
||||
let preFilter: 'all' | 'workspace' | 'hub' = 'all'
|
||||
let loading = false
|
||||
let small = false
|
||||
let open = false
|
||||
|
||||
$: small = kind === 'preprocessor' || kind === 'failure'
|
||||
</script>
|
||||
|
||||
<Menu
|
||||
transitionDuration={0}
|
||||
pointerDown
|
||||
bind:show={open}
|
||||
noMinW
|
||||
placement="bottom-center"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<!-- <Menu transitionDuration={0} pointerDown bind:show={open} noMinW {placement} let:close> -->
|
||||
|
||||
<!-- {floatingConfig}
|
||||
floatingClasses="mt-2"
|
||||
containerClasses="border rounded-lg shadow-lg bg-surface"
|
||||
noTransition
|
||||
shouldUsePortal={true} -->
|
||||
|
||||
<PopupV2 {floatingConfig} bind:open let:close target="#flow-editor">
|
||||
<svelte:fragment let:pointerdown let:pointerup slot="button">
|
||||
<button
|
||||
title="Add step"
|
||||
title={`Add ${
|
||||
kind === 'failure'
|
||||
? ' failure module '
|
||||
: kind === 'preprocessor'
|
||||
? 'preprocessor step'
|
||||
: kind === 'trigger'
|
||||
? 'trigger'
|
||||
: 'step'
|
||||
}`}
|
||||
id={`flow-editor-add-step-${index}`}
|
||||
type="button"
|
||||
class={twMerge(
|
||||
'w-5 h-5 flex items-center justify-center',
|
||||
'outline-[1px] outline dark:outline-gray-500 outline-gray-300',
|
||||
'text-secondary',
|
||||
'bg-surface focus:outline-none hover:bg-surface-hover rounded '
|
||||
'bg-surface focus:outline-none hover:bg-surface-hover rounded '
|
||||
)}
|
||||
on:pointerdown|preventDefault|stopPropagation={pointerdown}
|
||||
on:pointerup={pointerup}
|
||||
>
|
||||
<Cross size={12} />
|
||||
{#if kind === 'trigger'}
|
||||
<Zap size={12} />
|
||||
{:else}
|
||||
<Cross size={12} />
|
||||
{/if}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
<div id="flow-editor-insert-module">
|
||||
<StepGen on:insert {index} bind:funcDesc bind:open {close} {modules} {disableAi} />
|
||||
<!-- FOO -->
|
||||
<div
|
||||
id="flow-editor-insert-module"
|
||||
class="flex flex-col h-[400px] {small ? 'w-[450px]' : 'w-[650px]'} pt-1 pr-1 pl-1 gap-1.5"
|
||||
on:wheel={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
role="none"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<StepGenQuick on:insert bind:funcDesc {preFilter} {loading} />
|
||||
{#if selectedKind != 'preprocessor' && selectedKind != 'flow'}
|
||||
<ToggleHubWorkspaceQuick bind:selected={preFilter} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if funcDesc.length === 0}
|
||||
<div class="font-mono divide-y text-xs w-full text-secondary">
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'script')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Code size={14} />
|
||||
Action
|
||||
</button>
|
||||
{#if customUi?.triggers != false && trigger}
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'trigger')
|
||||
<div class="flex flex-row grow min-h-0">
|
||||
{#if kind === 'script' || kind == 'trigger'}
|
||||
<div class="flex-none flex flex-col text-xs text-primary">
|
||||
<TopLevelNode
|
||||
label="Action"
|
||||
selected={selectedKind === 'script'}
|
||||
on:select={() => {
|
||||
selectedKind = 'script'
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Zap size={14} />
|
||||
Trigger
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'approval')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<CheckCircle2 size={14} />
|
||||
Approval/Prompt
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'forloop')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Repeat size={14} />
|
||||
|
||||
For Loop
|
||||
</button>
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'whileloop')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Repeat size={14} />
|
||||
|
||||
While Loop
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'branchone')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<GitBranch size={14} />
|
||||
Branch to one
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'branchall')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<GitBranch size={14} />
|
||||
|
||||
Branch to all
|
||||
</button>
|
||||
|
||||
{#if customUi?.flowNode != false}
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover rounded-none whitespace-nowrap flex flex-row gap-2 items-center"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'flow')
|
||||
/>
|
||||
{#if customUi?.triggers != false && allowTrigger}
|
||||
<TopLevelNode
|
||||
label="Trigger"
|
||||
selected={selectedKind === 'trigger'}
|
||||
on:select={() => {
|
||||
selectedKind = 'trigger'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<TopLevelNode
|
||||
label="Approval/Prompt"
|
||||
selected={selectedKind === 'approval'}
|
||||
on:select={() => {
|
||||
selectedKind = 'approval'
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<BarsStaggered size={14} />
|
||||
Flow
|
||||
</button>
|
||||
{/if}
|
||||
{#if stop}
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover inline-flex gap-2.5"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'end')
|
||||
/>
|
||||
{#if customUi?.flowNode != false}
|
||||
<TopLevelNode
|
||||
label="Flow"
|
||||
selected={selectedKind === 'flow'}
|
||||
on:select={() => {
|
||||
selectedKind = 'flow'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if stop}
|
||||
<TopLevelNode
|
||||
label="End Flow"
|
||||
selected={selectedKind === 'script'}
|
||||
on:select={() => {
|
||||
selectedKind = 'script'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<TopLevelNode
|
||||
label="For Loop"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'forloop' })
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Square size={14} />
|
||||
End Flow
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="While Loop"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'whileloop' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="Branch to one"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'branchone' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="Branch to all"
|
||||
on:select={() => {
|
||||
close(null)
|
||||
dispatch('new', { kind: 'branchall' })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<FlowInputsQuick
|
||||
{selectedKind}
|
||||
bind:loading
|
||||
filter={funcDesc}
|
||||
{modules}
|
||||
{index}
|
||||
{disableAi}
|
||||
{funcDesc}
|
||||
{kind}
|
||||
on:close={() => {
|
||||
close(null)
|
||||
}}
|
||||
on:new
|
||||
on:pickScript
|
||||
on:pickFlow
|
||||
{preFilter}
|
||||
{small}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
</PopupV2>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Menu } from '$lib/components/common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import StepGen from '$lib/components/copilot/StepGen.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { Zap } from 'lucide-svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
export let open: boolean | undefined = undefined
|
||||
export let index: number
|
||||
export let funcDesc = ''
|
||||
export let modules: FlowModule[]
|
||||
export let disableAi = false
|
||||
|
||||
$: !open && (funcDesc = '')
|
||||
</script>
|
||||
|
||||
<Menu
|
||||
transitionDuration={0}
|
||||
pointerDown
|
||||
bind:show={open}
|
||||
noMinW
|
||||
placement="bottom-center"
|
||||
let:close
|
||||
>
|
||||
<button
|
||||
title="Add a Trigger"
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class="text-secondary bg-surface outline-[1px] outline dark:outline-gray-500 outline-gray-300 rotate-180 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded text-sm w-[20px] h-[20px] flex items-center justify-center"
|
||||
>
|
||||
<Zap size={12} />
|
||||
</button>
|
||||
{#if !disableAi}
|
||||
<StepGen {index} bind:funcDesc bind:open {close} {modules} trigger on:insert />
|
||||
{/if}
|
||||
{#if funcDesc.length === 0}
|
||||
<div class="font-mono divide-y text-xs w-full text-secondary whitespace-nowrap">
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover flex flex-row items-center gap-2"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'trigger')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Zap size={14} />Trigger
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</Menu>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let label: string
|
||||
export let lang: SupportedLanguage | 'docker' | 'javascript' | undefined = undefined
|
||||
export let selected = false
|
||||
export let eeRestricted: boolean
|
||||
export let enterpriseLangs: string[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
function handleKeydown(event: KeyboardEvent & { currentTarget: EventTarget & Window }) {
|
||||
if (selected && event.key === 'Enter') {
|
||||
click()
|
||||
}
|
||||
}
|
||||
|
||||
function click() {
|
||||
if (eeRestricted) {
|
||||
sendUserToast(
|
||||
`The languages ${enterpriseLangs.join(', ')} are only available on the enterprise edition`,
|
||||
true
|
||||
)
|
||||
return
|
||||
}
|
||||
dispatch('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'px-3 py-2 gap-2 w-full text-left hover:bg-surface-hover flex flex-row items-center transition-all rounded-md',
|
||||
selected ? 'bg-surface-hover' : ''
|
||||
)}
|
||||
on:click={click}
|
||||
role="menuitem"
|
||||
>
|
||||
{#if lang}
|
||||
<LanguageIcon {lang} width={14} height={14} />
|
||||
{/if}
|
||||
<span
|
||||
class="grow truncate text-left text-2xs font-normal {eeRestricted
|
||||
? 'text-secondary'
|
||||
: 'text-primary'}"
|
||||
>
|
||||
{label}{#if eeRestricted} (EE){/if}
|
||||
</span>
|
||||
{#if selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import TopLevelNode from './TopLevelNode.svelte'
|
||||
|
||||
export let label: string
|
||||
export let selected = false
|
||||
const dispatch = createEventDispatcher()
|
||||
function handleKeydown(event: KeyboardEvent & { currentTarget: EventTarget & Window }) {
|
||||
if (selected && event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
click()
|
||||
}
|
||||
}
|
||||
|
||||
function click() {
|
||||
dispatch('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<TopLevelNode class="px-3" {label} {selected} returnIcon on:select={click} />
|
||||
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { APP_TO_ICON_COMPONENT } from '$lib/components/icons'
|
||||
import { IntegrationService, ScriptService, type HubScriptKind } from '$lib/gen'
|
||||
import { Circle } from 'lucide-svelte'
|
||||
|
||||
export let kind: HubScriptKind & string = 'script'
|
||||
export let filter = ''
|
||||
|
||||
export let loading = false
|
||||
export let selected: number | undefined = undefined
|
||||
let hubNotAvailable = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let appFilter: string | undefined = undefined
|
||||
export let items: {
|
||||
path: string
|
||||
summary: string
|
||||
id: number
|
||||
version_id: number
|
||||
ask_id: number
|
||||
app: string
|
||||
kind: HubScriptKind
|
||||
}[] = []
|
||||
|
||||
export let apps: string[] = []
|
||||
let allApps: string[] = []
|
||||
$: applyFilter(filter, kind, appFilter)
|
||||
$: getAllApps(kind)
|
||||
|
||||
async function getAllApps(filterKind: typeof kind) {
|
||||
try {
|
||||
hubNotAvailable = false
|
||||
allApps = (
|
||||
await IntegrationService.listHubIntegrations({
|
||||
kind: filterKind
|
||||
})
|
||||
).map((x) => x.name)
|
||||
apps = allApps
|
||||
} catch (err) {
|
||||
console.error('Hub is not available')
|
||||
allApps = []
|
||||
apps = []
|
||||
hubNotAvailable = true
|
||||
}
|
||||
}
|
||||
|
||||
let startTs = 0
|
||||
async function applyFilter(
|
||||
filter: string,
|
||||
filterKind: typeof kind,
|
||||
appFilter: string | undefined
|
||||
) {
|
||||
try {
|
||||
loading = true
|
||||
hubNotAvailable = false
|
||||
const ts = Date.now()
|
||||
startTs = ts
|
||||
await new Promise((resolved, rejected) => setTimeout(resolved, 200))
|
||||
if (ts < startTs) return
|
||||
const scripts =
|
||||
filter.length > 0
|
||||
? await ScriptService.queryHubScripts({
|
||||
text: `${filter}`,
|
||||
limit: 40,
|
||||
kind: filterKind
|
||||
})
|
||||
: (
|
||||
await ScriptService.getTopHubScripts({
|
||||
limit: 40,
|
||||
kind: filterKind,
|
||||
app: appFilter
|
||||
})
|
||||
).asks ?? []
|
||||
|
||||
const mappedItems = scripts.map(
|
||||
(x: {
|
||||
summary: string
|
||||
version_id: number
|
||||
id: number
|
||||
ask_id: number
|
||||
app: string
|
||||
kind: HubScriptKind
|
||||
}) => ({
|
||||
...x,
|
||||
path: `hub/${x.version_id}/${x.app}/${x.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
|
||||
summary: `${x.summary} (${x.app})`
|
||||
})
|
||||
)
|
||||
if (filter.length > 0) {
|
||||
apps = Array.from(new Set(mappedItems?.map((x) => x.app) ?? [])).sort()
|
||||
} else {
|
||||
apps = allApps
|
||||
}
|
||||
|
||||
items = appFilter ? mappedItems.filter((x) => x.app === appFilter) : mappedItems
|
||||
|
||||
if (ts === startTs) {
|
||||
loading = false
|
||||
}
|
||||
|
||||
hubNotAvailable = false
|
||||
} catch (err) {
|
||||
hubNotAvailable = true
|
||||
console.error('Hub not available')
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (
|
||||
selected != undefined &&
|
||||
items &&
|
||||
selected >= 0 &&
|
||||
selected < items?.length! &&
|
||||
e.key === 'Enter'
|
||||
) {
|
||||
e.preventDefault()
|
||||
let item = items![selected]
|
||||
dispatch('pickScript', item)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
{#if hubNotAvailable}
|
||||
<div class="text-2xs text-red-400 ftext-2xs font-light text-center py-2 px-3 items-center">
|
||||
Hub not available
|
||||
</div>
|
||||
{:else if loading}
|
||||
{#each Array(15).fill(0) as _}
|
||||
<Skeleton layout={[0.1, [1.5]]} />
|
||||
{/each}
|
||||
{:else if items.length > 0 && apps.length > 0}
|
||||
<ul>
|
||||
{#each items as item, index (item.path)}
|
||||
<li class="w-full">
|
||||
<button
|
||||
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
|
||||
selected
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click={() => dispatch('pickScript', item)}
|
||||
>
|
||||
<div class={classNames('flex justify-center items-center')}>
|
||||
{#if item['app'] in APP_TO_ICON_COMPONENT}
|
||||
<svelte:component this={APP_TO_ICON_COMPONENT[item['app']]} height={14} width={14} />
|
||||
{:else}
|
||||
<div
|
||||
class="w-[14px] h-[14px] text-gray-400 flex flex-row items-center justify-center"
|
||||
>
|
||||
<Circle size="12" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<span class="grow truncate text-left text-2xs text-primary font-normal">
|
||||
{item.summary ?? ''}
|
||||
</span>
|
||||
{#if index === selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if items.length == 40}
|
||||
<div class="text-2xs text-tercary font-extralight text-center py-2 px-3 items-center">
|
||||
There are more items than being displayed. Refine your search.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import { CheckCircle2, ChevronRight, Code, GitBranch, Repeat, Square, Zap } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let label: string
|
||||
export let selected = false
|
||||
export let returnIcon = false
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'w-full text-left py-2 px-1.5 hover:bg-surface-hover text-xs font-medium transition-all whitespace-nowrap flex flex-row gap-2 items-center rounded-md',
|
||||
selected ? 'bg-surface-hover' : '',
|
||||
$$props.class
|
||||
)}
|
||||
on:pointerdown={() => dispatch('select', label)}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span class="grow flex items-center gap-2">
|
||||
{#if label === 'Action'}
|
||||
<Code size={14} />
|
||||
Action
|
||||
<ChevronRight size={12} class="ml-auto" color="#4c566a" />
|
||||
{:else if label === 'Trigger'}
|
||||
<Zap size={14} />
|
||||
Trigger
|
||||
<ChevronRight size={12} class="ml-auto" color="#4c566a" />
|
||||
{:else if label === 'Approval/Prompt'}
|
||||
<CheckCircle2 size={14} />
|
||||
Approval/Prompt
|
||||
<ChevronRight size={12} class="ml-auto" color="#4c566a" />
|
||||
{:else if label === 'Flow'}
|
||||
<BarsStaggered size={14} />
|
||||
Flow
|
||||
<ChevronRight size={12} class="ml-auto" color="#4c566a" />
|
||||
{:else if label === 'End Flow'}
|
||||
<Square size={14} />
|
||||
End Flow
|
||||
{:else if label === 'For Loop'}
|
||||
<Repeat size={14} />
|
||||
For Loop
|
||||
{:else if label === 'While Loop'}
|
||||
<Repeat size={14} />
|
||||
While Loop
|
||||
{:else if label === 'Branch to one'}
|
||||
<GitBranch size={14} />
|
||||
Branch to one
|
||||
{:else if label === 'Branch to all'}
|
||||
<GitBranch size={14} />
|
||||
Branch to all
|
||||
{/if}
|
||||
</span>
|
||||
{#if returnIcon && selected}
|
||||
<kbd class="!text-xs text-right">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { Code2 } from 'lucide-svelte'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
|
||||
export let kind: 'script' | 'trigger' | 'approval' | 'failure' | 'flow' | 'preprocessor' =
|
||||
'script'
|
||||
export let isTemplate: boolean | undefined = undefined
|
||||
export let selected: number | undefined = undefined
|
||||
|
||||
type Item = {
|
||||
path: string
|
||||
summary?: string
|
||||
description?: string
|
||||
hash?: string
|
||||
}
|
||||
|
||||
let items: Item[] | undefined = undefined
|
||||
|
||||
let filteredItems: (Item & { marked?: string })[] | undefined = undefined
|
||||
export let filteredWithOwner: (Item & { marked?: string })[] | undefined = undefined
|
||||
|
||||
export let filter = ''
|
||||
export let owners: string[] = []
|
||||
|
||||
$: $workspaceStore && kind && loadItems()
|
||||
|
||||
async function loadItems(): Promise<void> {
|
||||
items =
|
||||
kind == 'flow'
|
||||
? await FlowService.listFlows({ workspace: $workspaceStore! })
|
||||
: await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
kinds: kind,
|
||||
isTemplate
|
||||
})
|
||||
}
|
||||
|
||||
export let ownerFilter:
|
||||
| { kind: 'inline' | 'owner' | 'integrations'; name: string | undefined }
|
||||
| undefined = undefined
|
||||
|
||||
$: if ($workspaceStore) {
|
||||
ownerFilter = undefined
|
||||
}
|
||||
|
||||
$: owners = Array.from(
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort((a, b) => {
|
||||
if (a.startsWith('u/') && !b.startsWith('u/')) return -1
|
||||
if (b.startsWith('u/') && !a.startsWith('u/')) return 1
|
||||
|
||||
if (a.startsWith('f/') && !b.startsWith('f/')) return -1
|
||||
if (b.startsWith('f/') && !a.startsWith('f/')) return 1
|
||||
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let lockHash = false
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (
|
||||
selected != undefined &&
|
||||
filteredItems &&
|
||||
selected >= 0 &&
|
||||
selected < filteredItems.length &&
|
||||
e.key === 'Enter'
|
||||
) {
|
||||
e.preventDefault()
|
||||
let item = filteredItems[selected]
|
||||
dispatch('pickScript', { path: item.path, hash: lockHash ? item.hash : undefined })
|
||||
}
|
||||
}
|
||||
|
||||
$: filteredWithOwner =
|
||||
ownerFilter != undefined
|
||||
? filteredItems?.filter((x) => x.path.startsWith(ownerFilter?.name!))
|
||||
: filteredItems
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
{items}
|
||||
bind:filteredItems
|
||||
f={(x) => (emptyString(x.summary) ? x.path : x.summary + ' (' + x.path + ')')}
|
||||
/>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
{#if filteredItems}
|
||||
{#if filteredItems.length == 0}
|
||||
<div class="text-2xs text-tertiary font-light text-center py-2 px-3 items-center">
|
||||
{kind == 'flow' ? 'No flows found.' : 'No scripts found.'}
|
||||
</div>
|
||||
{/if}
|
||||
<ul>
|
||||
{#each filteredWithOwner ?? [] as { path, hash, summary, marked }, index}
|
||||
<li class="w-full">
|
||||
<button
|
||||
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
|
||||
selected
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
dispatch('pickScript', { path, hash: lockHash ? hash : undefined })
|
||||
}}
|
||||
>
|
||||
{#if kind == 'flow'}
|
||||
<BarsStaggered size={14} />
|
||||
{:else}
|
||||
<Code2 size={14} />
|
||||
{/if}
|
||||
<span class="grow min-w-0 truncate text-left text-2xs text-primary font-normal">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}</span
|
||||
>
|
||||
{#if index === selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
{#each Array(10).fill(0) as _}
|
||||
<Skeleton layout={[0.5, [1.5]]} />
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -41,4 +41,5 @@ export type FlowEditorContext = {
|
||||
initialPath: string
|
||||
flowInputsStore: Writable<FlowInput>
|
||||
customUi: FlowBuilderWhitelabelCustomUi
|
||||
insertButtonOpen: Writable<boolean>
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
export let selectedId: Writable<string | undefined> = writable<string | undefined>(undefined)
|
||||
|
||||
export let insertable = false
|
||||
export let moving: string | undefined = undefined
|
||||
export let scroll = false
|
||||
export let moving: string | undefined = undefined
|
||||
|
||||
// Download: display a top level button to open the graph in a new tab
|
||||
export let download = false
|
||||
@@ -266,6 +266,9 @@
|
||||
</div>
|
||||
{:else}
|
||||
<SvelteFlow
|
||||
on:paneclick={(e) => {
|
||||
window.dispatchEvent(new Event('focus'))
|
||||
}}
|
||||
{nodes}
|
||||
{edges}
|
||||
{edgeTypes}
|
||||
|
||||
@@ -549,4 +549,5 @@ export default function graphBuilder(
|
||||
error: e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder'
|
||||
import { getStraightLinePath } from '../utils'
|
||||
import InsertTriggerButton from '$lib/components/flows/map/InsertTriggerButton.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let sourceX: number
|
||||
@@ -47,33 +46,44 @@
|
||||
const { useDataflow } = getContext<{
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
}>('FlowGraphContext')
|
||||
|
||||
let menuOpen = false
|
||||
</script>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
{#if data?.insertable && !$useDataflow && !data?.moving}
|
||||
<div
|
||||
class={twMerge('edgeButtonContainer nodrag nopan top-0', menuOpen ? 'z-50' : '')}
|
||||
class={twMerge('edgeButtonContainer nodrag nopan top-0')}
|
||||
style:transform="translate(-50%, 50%) translate({sourceX}px,{sourceY + 2}px)"
|
||||
>
|
||||
<InsertModuleButton
|
||||
disableAi={data.disableAi}
|
||||
index={data.index ?? 0}
|
||||
trigger={data.enableTrigger}
|
||||
allowTrigger={data.enableTrigger}
|
||||
modules={data?.modules ?? []}
|
||||
on:new={(e) => {
|
||||
data?.eventHandlers.insert({ modules: data.modules, index: data.index, detail: e.detail })
|
||||
}}
|
||||
on:insert={(e) => {
|
||||
// console.log('new', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
script: e.detail,
|
||||
detail: 'script'
|
||||
kind: e.detail.kind,
|
||||
inlineScript: e.detail.inlineScript
|
||||
})
|
||||
}}
|
||||
on:pickScript={(e) => {
|
||||
// console.log('pickScript', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
script: e.detail
|
||||
})
|
||||
}}
|
||||
on:pickFlow={(e) => {
|
||||
// console.log('pickFlow', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
flow: e.detail
|
||||
})
|
||||
}}
|
||||
bind:open={menuOpen}
|
||||
/>
|
||||
</div>
|
||||
{#if data.enableTrigger}
|
||||
@@ -81,23 +91,34 @@
|
||||
class="edgeButtonContainer nodrag nopan"
|
||||
style:transform="translate(100%, 50%) translate({sourceX}px,{sourceY + 2}px)"
|
||||
>
|
||||
<InsertTriggerButton
|
||||
<InsertModuleButton
|
||||
disableAi={data.disableAi}
|
||||
on:new={(e) => {
|
||||
// console.log('new', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
detail: e.detail
|
||||
kind: e.detail.kind,
|
||||
inlineScript: e.detail.inlineScript
|
||||
})
|
||||
}}
|
||||
on:insert={(e) => {
|
||||
on:pickScript={(e) => {
|
||||
// console.log('pickScript', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
script: e.detail,
|
||||
detail: 'script'
|
||||
script: e.detail
|
||||
})
|
||||
}}
|
||||
on:pickFlow={(e) => {
|
||||
// console.log('pickFlow', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
flow: e.detail
|
||||
})
|
||||
}}
|
||||
kind="trigger"
|
||||
index={data?.index ?? 0}
|
||||
modules={data?.modules ?? []}
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script lang="ts">
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import { Cross } from 'lucide-svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { getStateColor } from '../../util'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import FlowCopilotButton from '$lib/components/flows/map/FlowCopilotButton.svelte'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
|
||||
export let data: {
|
||||
insertable: boolean
|
||||
eventHandlers: GraphEventHandlers
|
||||
modules: FlowModule[]
|
||||
disableAi: boolean
|
||||
hasPreprocessor: boolean
|
||||
insertable: boolean
|
||||
modules: FlowModule[]
|
||||
moving: string | undefined
|
||||
eventHandlers: GraphEventHandlers
|
||||
index: number
|
||||
enableTrigger: boolean
|
||||
disableAi: boolean
|
||||
disableMoveIds: string[]
|
||||
}
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
@@ -29,25 +32,29 @@
|
||||
{/if}
|
||||
{#if data.insertable && !data.hasPreprocessor}
|
||||
<div class="absolute -top-8 left-1/2 transform -translate-x-1/2 z-10">
|
||||
<button
|
||||
on:click={(e) => {
|
||||
data.eventHandlers?.insert({
|
||||
<InsertModuleButton
|
||||
disableAi={data.disableAi}
|
||||
index={data.index ?? 0}
|
||||
modules={data?.modules ?? []}
|
||||
kind="preprocessor"
|
||||
on:new={(e) => {
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
kind: e.detail.kind,
|
||||
inlineScript: e.detail.inlineScript,
|
||||
detail: 'preprocessor'
|
||||
})
|
||||
}}
|
||||
title="Add preprocessor step"
|
||||
id={`flow-editor-add-preprocessor`}
|
||||
type="button"
|
||||
class={twMerge(
|
||||
'w-5 h-5 flex items-center justify-center',
|
||||
'outline-[1px] outline dark:outline-gray-500 outline-gray-300',
|
||||
'text-secondary',
|
||||
'bg-surface focus:outline-none hover:bg-surface-hover rounded '
|
||||
)}
|
||||
>
|
||||
<Cross size={12} />
|
||||
</button>
|
||||
on:pickScript={(e) => {
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
script: e.detail,
|
||||
detail: 'preprocessor'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<VirtualItem
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { Folder, User, Circle } from 'lucide-svelte'
|
||||
import { APP_TO_ICON_COMPONENT } from '../icons'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let filters: string[]
|
||||
export let selectedFilter:
|
||||
| { kind: 'owner' | 'integrations'; name: string | undefined }
|
||||
| undefined = undefined
|
||||
$: selectedAppFilter = selectedFilter?.kind === 'integrations' ? selectedFilter?.name : undefined
|
||||
|
||||
export let resourceType = false
|
||||
|
||||
function getIconComponent(name: string) {
|
||||
return APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if Array.isArray(filters) && filters.length > 0}
|
||||
{#each filters as filter (filter)}
|
||||
<div>
|
||||
<button
|
||||
class={twMerge(
|
||||
'w-full text-left py-2 px-3 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center rounded-md',
|
||||
filter === selectedAppFilter ? 'bg-surface-hover' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
selectedFilter =
|
||||
selectedAppFilter == filter ? undefined : { kind: 'integrations', name: filter }
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-center flex-row items-center gap-2">
|
||||
{#if resourceType}
|
||||
{@const icon = getIconComponent(filter)}
|
||||
{#if icon}
|
||||
<svelte:component this={icon} height="14px" width="14px" />
|
||||
{:else}
|
||||
<div
|
||||
class="w-[14px] h-[14px] text-gray-400 flex flex-row items-center justify-center"
|
||||
>
|
||||
<Circle size="12" />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if filter.startsWith('u/')}
|
||||
<User class="mr-0.5" size={14} />
|
||||
{:else if filter.startsWith('f/')}
|
||||
<Folder class="mr-0.5" size={14} />
|
||||
{/if}
|
||||
<span class="text-left text-2xs text-primary font-normal">{filter}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
export let white = false
|
||||
export let size = '24px'
|
||||
export let color: string | undefined = undefined
|
||||
export let spin: 'slow' | 'medium' | 'fast' | 'veryfast' | undefined = undefined
|
||||
|
||||
function hslToHex(h, s, l) {
|
||||
s /= 100
|
||||
l /= 100
|
||||
|
||||
let c = (1 - Math.abs(2 * l - 1)) * s
|
||||
let x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
||||
let m = l - c / 2
|
||||
let r = 0
|
||||
let g = 0
|
||||
let b = 0
|
||||
|
||||
if (0 <= h && h < 60) {
|
||||
r = c
|
||||
g = x
|
||||
b = 0
|
||||
} else if (60 <= h && h < 120) {
|
||||
r = x
|
||||
g = c
|
||||
b = 0
|
||||
} else if (120 <= h && h < 180) {
|
||||
r = 0
|
||||
g = c
|
||||
b = x
|
||||
} else if (180 <= h && h < 240) {
|
||||
r = 0
|
||||
g = x
|
||||
b = c
|
||||
} else if (240 <= h && h < 300) {
|
||||
r = x
|
||||
g = 0
|
||||
b = c
|
||||
} else if (300 <= h && h < 360) {
|
||||
r = c
|
||||
g = 0
|
||||
b = x
|
||||
}
|
||||
|
||||
let rs = Math.round((r + m) * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')
|
||||
let gs = Math.round((g + m) * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')
|
||||
let bs = Math.round((b + m) * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')
|
||||
|
||||
return `#${rs}${gs}${bs}`
|
||||
}
|
||||
|
||||
function hexToHsl(hex) {
|
||||
let r: number = parseInt(hex.slice(1, 3), 16) / 255
|
||||
let g: number = parseInt(hex.slice(3, 5), 16) / 255
|
||||
let b: number = parseInt(hex.slice(5, 7), 16) / 255
|
||||
|
||||
const max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b)
|
||||
let h,
|
||||
s,
|
||||
l = (max + min) / 2
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0 // Achromatic
|
||||
} else {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0)
|
||||
break
|
||||
case g:
|
||||
h = (b - r) / d + 2
|
||||
break
|
||||
case b:
|
||||
h = (r - g) / d + 4
|
||||
break
|
||||
}
|
||||
h /= 6
|
||||
}
|
||||
|
||||
return [h * 360, s * 100, l * 100]
|
||||
}
|
||||
|
||||
function reduceSaturation(hex: string, reductionPercent: number) {
|
||||
// Convert HEX to HSL
|
||||
|
||||
// Convert the hex to HSL
|
||||
let [h, s, l] = hexToHsl(hex)
|
||||
|
||||
// Reduce the saturation by the specified percentage
|
||||
l = Math.max(0, l - reductionPercent)
|
||||
|
||||
// Convert back to hex
|
||||
return hslToHex(h, s, l)
|
||||
}
|
||||
|
||||
let lessSaturatedColor: string | undefined
|
||||
|
||||
$: color ? (lessSaturatedColor = reduceSaturation(color, -16)) : (lessSaturatedColor = undefined)
|
||||
</script>
|
||||
|
||||
<!-- SVG Icon with customizable color -->
|
||||
<svg
|
||||
class={$$props.class}
|
||||
class:animate-[spin_2s_linear_infinite]={spin === 'veryfast'}
|
||||
class:animate-[spin_5s_linear_infinite]={spin === 'fast'}
|
||||
class:animate-[spin_15s_linear_infinite]={spin === 'medium'}
|
||||
class:animate-[spin_50s_linear_infinite]={spin === 'slow'}
|
||||
version="1.1"
|
||||
id="Calque_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
style="enable-background:new 0 0 256 256;"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<!-- Use color or fallback to defaults (white or blue) -->
|
||||
<polygon
|
||||
fill={lessSaturatedColor || (white ? '#cccccc' : '#bcd4fc')}
|
||||
points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22"
|
||||
/>
|
||||
<polygon
|
||||
fill={color || (white ? '#ffffff' : '#3b82f6')}
|
||||
points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02"
|
||||
/>
|
||||
<polygon
|
||||
fill={color || (white ? '#ffffff' : '#3b82f6')}
|
||||
points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46"
|
||||
/>
|
||||
<polygon
|
||||
fill={color || (white ? '#ffffff' : '#3b82f6')}
|
||||
points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22"
|
||||
/>
|
||||
<polygon
|
||||
fill={lessSaturatedColor || (white ? '#cccccc' : '#bcd4fc')}
|
||||
points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21"
|
||||
/>
|
||||
<polygon
|
||||
fill={lessSaturatedColor || (white ? '#cccccc' : '#bcd4fc')}
|
||||
points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -18,6 +18,7 @@ import S3Icon from './S3Icon.svelte'
|
||||
import Slack from './Slack.svelte'
|
||||
import TogglIcon from './TogglIcon.svelte'
|
||||
import WindmillIcon from './WindmillIcon.svelte'
|
||||
import WindmillIcon2 from './WindmillIcon2.svelte'
|
||||
import MailchimpIcon from './MailchimpIcon.svelte'
|
||||
import SendgridIcon from './SendgridIcon.svelte'
|
||||
import SendflakeIcon from './SendflakeIcon.svelte'
|
||||
@@ -211,6 +212,7 @@ export {
|
||||
Slack,
|
||||
TogglIcon,
|
||||
WindmillIcon,
|
||||
WindmillIcon2,
|
||||
MailchimpIcon,
|
||||
SendgridIcon,
|
||||
LinkedinIcon,
|
||||
|
||||
@@ -350,6 +350,27 @@ export async function main() {
|
||||
}
|
||||
`
|
||||
|
||||
export const BUN_INIT_CODE_TRIGGER = `import * as wmill from "windmill-client"
|
||||
|
||||
export async function main() {
|
||||
|
||||
// A common trigger script would follow this pattern:
|
||||
// 1. Get the last saved state
|
||||
// const state = await wmill.getState()
|
||||
// 2. Get the actual state from the external service
|
||||
// const newState = await (await fetch('https://hacker-news.firebaseio.com/v0/topstories.json')).json()
|
||||
// 3. Compare the two states and update the internal state
|
||||
// await wmill.setState(newState)
|
||||
// 4. Return the new rows
|
||||
// return range from (state to newState)
|
||||
|
||||
return [1,2,3]
|
||||
|
||||
// In subsequent scripts, you may refer to each row/value returned by the trigger script using
|
||||
// 'flow_input.iter.value'
|
||||
}
|
||||
`
|
||||
|
||||
export const GO_INIT_CODE_TRIGGER = `package inner
|
||||
|
||||
import (
|
||||
@@ -715,7 +736,9 @@ export function initialCode(
|
||||
} else if (language == 'ansible') {
|
||||
return ANSIBLE_PLAYBOOK_INIT_CODE
|
||||
} else if (language == 'bun' || language == 'bunnative') {
|
||||
if (language == 'bunnative' || subkind === 'bunnative') {
|
||||
if (kind == 'trigger') {
|
||||
return BUN_INIT_CODE_TRIGGER
|
||||
} else if (language == 'bunnative' || subkind === 'bunnative') {
|
||||
return BUNNATIVE_INIT_CODE
|
||||
} else if (kind === 'approval') {
|
||||
return BUN_INIT_CODE_APPROVAL
|
||||
@@ -723,8 +746,7 @@ export function initialCode(
|
||||
return BUN_FAILURE_MODULE_CODE
|
||||
} else if (subkind === 'preprocessor') {
|
||||
return BUN_PREPROCESSOR_MODULE_CODE
|
||||
}
|
||||
if (subkind === 'flow') {
|
||||
} else if (subkind === 'flow') {
|
||||
return BUN_INIT_CODE_CLEAR
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,8 @@
|
||||
saveDraft: () => {},
|
||||
initialPath: '',
|
||||
flowInputsStore: writable<FlowInput>({}),
|
||||
customUi: {}
|
||||
customUi: {},
|
||||
insertButtonOpen: writable(false)
|
||||
})
|
||||
|
||||
type LastEdit = {
|
||||
|
||||
Reference in New Issue
Block a user