feat(frontend): add listeners for frontend scripts

This commit is contained in:
Ruben Fiszel
2023-03-16 14:56:16 +01:00
parent ca3e3624c0
commit 597e38ef36
10 changed files with 123 additions and 13 deletions
+24
View File
@@ -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",
+2
View File
@@ -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
}
@@ -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:&nbsp;<Tooltip
>Script executed in the client's browser directly:&nbsp;<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
+2 -3
View File
@@ -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 = {