mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
feat(frontend): add listeners for frontend scripts
This commit is contained in:
Generated
+24
@@ -23,6 +23,7 @@
|
||||
"date-fns": "^2.29.3",
|
||||
"fast-equals": "^4.0.3",
|
||||
"highlight.js": "^11.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-svelte": "^0.115.0",
|
||||
"monaco-editor": "0.33.0",
|
||||
"monaco-editor-workers": "0.33.0",
|
||||
@@ -44,6 +45,7 @@
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-zoom": "^3.0.2",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
@@ -1285,6 +1287,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/minimist": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
|
||||
@@ -4401,6 +4409,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.castarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||
@@ -9082,6 +9095,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
|
||||
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimist": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
|
||||
@@ -11358,6 +11377,11 @@
|
||||
"p-locate": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash.castarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-zoom": "^3.0.2",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
@@ -78,6 +79,7 @@
|
||||
"date-fns": "^2.29.3",
|
||||
"fast-equals": "^4.0.3",
|
||||
"highlight.js": "^11.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-svelte": "^0.115.0",
|
||||
"monaco-editor": "0.33.0",
|
||||
"monaco-editor-workers": "0.33.0",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
export let id: string | undefined = undefined
|
||||
export let error: string = ''
|
||||
export let extraContext: Record<string, any> = {}
|
||||
export let key: string = ''
|
||||
|
||||
const { componentControl } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -63,6 +64,7 @@
|
||||
|
||||
async function handleConnection() {
|
||||
if (lastInput.type === 'connected') {
|
||||
console.log('connected', lastInput, `${id}-${key}`)
|
||||
$worldStore?.connect<any>(lastInput, onValueChange, id)
|
||||
} else if (lastInput.type === 'static' || lastInput.type == 'template') {
|
||||
value = await getValue(lastInput)
|
||||
@@ -121,6 +123,8 @@
|
||||
|
||||
function onValueChange(newValue: any): void {
|
||||
if (lastInput.type === 'connected' && newValue !== undefined && newValue !== null) {
|
||||
console.log(newValue, id, lastInput)
|
||||
|
||||
const { connection } = lastInput
|
||||
if (!connection) {
|
||||
// No connection
|
||||
|
||||
@@ -98,10 +98,6 @@
|
||||
|
||||
$: fields && (lazyStaticValues = computeStaticValues())
|
||||
$: (runnableInputValues || extraQueryParams || args) && testJobLoader && refreshIfAutoRefresh()
|
||||
$: runnable?.type == 'runnableByName' &&
|
||||
runnable.inlineScript?.language == 'frontend' &&
|
||||
($stateId || $state) &&
|
||||
refreshIfAutoRefresh()
|
||||
|
||||
function refreshIfAutoRefresh() {
|
||||
if (autoRefresh) {
|
||||
@@ -264,12 +260,24 @@
|
||||
$: result?.error && recordError(result.error)
|
||||
</script>
|
||||
|
||||
{#each Object.entries(fields ?? {}) as [key, v]}
|
||||
{#each Object.entries(fields ?? {}) as [key, v] (key)}
|
||||
{#if v.type != 'static' && v.type != 'user'}
|
||||
<InputValue {id} input={fields[key]} bind:value={runnableInputValues[key]} />
|
||||
<InputValue {key} {id} input={fields[key]} bind:value={runnableInputValues[key]} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if runnable?.type == 'runnableByName' && runnable.inlineScript?.language == 'frontend'}
|
||||
{#each runnable.inlineScript.refreshOn ?? [] as { id: tid, key } (`${tid}-${key}`)}
|
||||
{@const fkey = `${tid}-${key}`}
|
||||
<InputValue
|
||||
{id}
|
||||
key={fkey}
|
||||
input={{ type: 'connected', connection: { componentId: tid, path: key }, fieldType: 'any' }}
|
||||
bind:value={runnableInputValues[fkey]}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<TestJobLoader
|
||||
workspaceOverride={workspace}
|
||||
on:done={(e) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { isPlainObject } from 'lodash'
|
||||
import type { World } from '../../rx'
|
||||
|
||||
export function computeGlobalContext(
|
||||
@@ -45,6 +46,38 @@ function make_context_evaluator(
|
||||
return functor()
|
||||
}
|
||||
|
||||
function isSerializable(obj) {
|
||||
var isNestedSerializable
|
||||
function isPlain(val) {
|
||||
return (
|
||||
val == null ||
|
||||
typeof val === 'undefined' ||
|
||||
typeof val === 'string' ||
|
||||
typeof val === 'boolean' ||
|
||||
typeof val === 'number' ||
|
||||
Array.isArray(val) ||
|
||||
isPlainObject(val)
|
||||
)
|
||||
}
|
||||
if (!isPlain(obj)) {
|
||||
return false
|
||||
}
|
||||
for (var property in obj) {
|
||||
if (obj.hasOwnProperty(property)) {
|
||||
if (!isPlain(obj[property])) {
|
||||
return false
|
||||
}
|
||||
if (typeof obj[property] == 'object') {
|
||||
isNestedSerializable = isSerializable(obj[property])
|
||||
if (!isNestedSerializable) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export async function eval_like(
|
||||
text: string,
|
||||
context = {},
|
||||
@@ -60,7 +93,11 @@ export async function eval_like(
|
||||
throw new Error('Invalid key')
|
||||
}
|
||||
let o = worldStore?.newOutput('state', key, value)
|
||||
o?.set(value)
|
||||
if (isSerializable(value)) {
|
||||
o?.set(value, true)
|
||||
} else {
|
||||
o?.set('Not serializable object usable only by frontend scripts', true)
|
||||
}
|
||||
target[key] = value
|
||||
return true
|
||||
}
|
||||
|
||||
+12
-2
@@ -3,7 +3,6 @@
|
||||
import { classNames } from '$lib/utils'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { allsubIds } from '../../appUtils'
|
||||
|
||||
export let id: string
|
||||
@@ -15,7 +14,7 @@
|
||||
const { expanded, manuallyOpened, search, hasResult } =
|
||||
getContext<ContextPanelContext>('ContextPanel')
|
||||
|
||||
const { selectedComponent, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { selectedComponent, app, hoverStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: subids = allsubIds($app, id)
|
||||
$: inSearch =
|
||||
@@ -50,7 +49,18 @@
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class={$search == '' || inSearch ? '' : 'invisible h-0 overflow-hidden'}>
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
if (id !== $hoverStore) {
|
||||
$hoverStore = id
|
||||
}
|
||||
}}
|
||||
on:mouseout|stopPropagation={() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
$hoverStore = undefined
|
||||
}
|
||||
}}
|
||||
class={classNames(
|
||||
'flex items-center justify-between p-1 cursor-pointer border-b gap-1 truncate',
|
||||
hoverColor[color],
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs mb-1 mt-2"
|
||||
>Frontend only script: <Tooltip
|
||||
>Script executed in the client's browser directly: <Tooltip
|
||||
>Frontend scripts are executed in the browser and can manipulate the app context directly</Tooltip
|
||||
></div
|
||||
>
|
||||
|
||||
@@ -87,6 +87,22 @@
|
||||
inlineScript.language == 'frontend' && worldStore
|
||||
? buildExtraLib($worldStore?.outputsById ?? {}, id, false, $state, true)
|
||||
: undefined
|
||||
|
||||
let refreshOn: string = inlineScript.refreshOn?.map((x) => `${x.id}.${x.key}`).join(' ') ?? ''
|
||||
|
||||
$: handleRefreshOn(refreshOn)
|
||||
|
||||
function handleRefreshOn(refreshOn: string) {
|
||||
if (refreshOn && refreshOn != '') {
|
||||
inlineScript.refreshOn = refreshOn
|
||||
.split(' ')
|
||||
.filter((x) => x.split('.').length == 2)
|
||||
.map((x) => {
|
||||
const [id, key] = x.split('.')
|
||||
return { id, key }
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if inlineScript.language != 'frontend'}
|
||||
@@ -190,6 +206,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if inlineScript.language == 'frontend'}
|
||||
<div class="flex flex-row text-xs px-1"
|
||||
>List of outputs to listen to (e.g 'a.result b.result'): <input
|
||||
on:keydown|stopPropagation
|
||||
type="text"
|
||||
bind:value={refreshOn}
|
||||
/></div
|
||||
>
|
||||
{/if}
|
||||
<div class="border h-full">
|
||||
{#if inlineScript.language != 'frontend'}
|
||||
<Editor
|
||||
|
||||
@@ -68,8 +68,6 @@ export function buildObservableWorld() {
|
||||
next: () => {}
|
||||
}
|
||||
} else if (inputSpec.type === 'connected') {
|
||||
const input = cachedInput(next, id)
|
||||
|
||||
const connection = inputSpec.connection
|
||||
|
||||
if (!connection) {
|
||||
@@ -81,6 +79,8 @@ export function buildObservableWorld() {
|
||||
}
|
||||
|
||||
const { componentId, path } = connection
|
||||
console.log('connection', connection)
|
||||
const input = cachedInput(next, `${componentId}-${path}-${id}`)
|
||||
|
||||
const [p] = path ? path.split('.')[0].split('[') : [undefined]
|
||||
|
||||
@@ -164,7 +164,6 @@ export function settableOutput<T>(state: Writable<number>, previousValue: T): Ou
|
||||
function set(x: T, force: boolean = false) {
|
||||
if (!deepEqual(value, x) || force) {
|
||||
state.update((x) => x + 1)
|
||||
|
||||
value = x
|
||||
subscribers.forEach((x) => x.next(value!))
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ export type InlineScript = {
|
||||
language: Preview.language | 'frontend'
|
||||
path?: string
|
||||
schema?: Schema
|
||||
refreshOn?: { id: string; key: string }[]
|
||||
}
|
||||
|
||||
export type App = {
|
||||
|
||||
Reference in New Issue
Block a user