mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
feat(frontend): App component triggers (#1303)
* feat(frontend): add trigger list * feat(frontend): add trigger list * feat(frontend): add support for refreshOn * feat(frontend): add support for refreshOn * feat(frontend): rework * feat(frontend): rework * feat(frontend): code cleanup
This commit is contained in:
@@ -125,8 +125,6 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
<span>CSS</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<div slot="content" class="h-full overflow-y-auto pb-4">
|
||||
<div slot="content" class="h-full overflow-y-auto">
|
||||
<TabContent class="overflow-auto h-full" value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
<SettingsPanel />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import GridPanel from './GridPanel.svelte'
|
||||
import PanelSection from './settingsPanel/common/PanelSection.svelte'
|
||||
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
|
||||
import BackgroundScriptTriggerList from './settingsPanel/triggerLists/BackgroundScriptTriggerList.svelte'
|
||||
|
||||
const { selectedComponent, app, stateId } = getContext<AppViewerContext>('AppViewerContext')
|
||||
</script>
|
||||
@@ -29,6 +30,16 @@
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<div class="p-2">
|
||||
{#if script.inlineScript}
|
||||
<BackgroundScriptTriggerList
|
||||
fields={script.fields}
|
||||
autoRefresh={script.autoRefresh}
|
||||
id={`bg_${index}`}
|
||||
bind:inlineScript={script.inlineScript}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if Object.keys(script.fields).length > 0}
|
||||
<PanelSection title={`Inputs`}>
|
||||
{#key $stateId}
|
||||
|
||||
@@ -55,6 +55,22 @@ export function getNextGridItemId(app: App): string {
|
||||
return id
|
||||
}
|
||||
|
||||
export function getAllRecomputeIdsForComponent(app: App, id: string) {
|
||||
const items = allItems(app.grid, app.subgrids)
|
||||
|
||||
const recomputedBy: string[] = []
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.data.type === 'buttoncomponent') {
|
||||
if (item.data.recomputeIds?.includes(id)) {
|
||||
recomputedBy.push(item.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return recomputedBy
|
||||
}
|
||||
|
||||
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
|
||||
const newComponent = {
|
||||
resizable: true,
|
||||
|
||||
+1
-10
@@ -3,7 +3,7 @@
|
||||
import type { Preview } from '$lib/gen'
|
||||
import { createEventDispatcher, getContext, onMount } from 'svelte'
|
||||
import type { AppViewerContext, InlineScript } from '../../types'
|
||||
import { CornerDownLeft, Maximize2, Plus, Trash2, X } from 'lucide-svelte'
|
||||
import { CornerDownLeft, Maximize2, Trash2 } from 'lucide-svelte'
|
||||
import InlineScriptEditorDrawer from './InlineScriptEditorDrawer.svelte'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import type { Schema } from '$lib/common'
|
||||
@@ -206,15 +206,6 @@
|
||||
</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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { clearResultAppInput } from '../../utils'
|
||||
import InlineScriptEditorDrawer from '../inlineScriptsPanel/InlineScriptEditorDrawer.svelte'
|
||||
import ComponentTriggerList from './triggerLists/ComponentTriggerList.svelte'
|
||||
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -66,6 +67,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript}
|
||||
<ComponentTriggerList fields={appInput.fields} />
|
||||
{/if}
|
||||
{#if appInput.runnable?.type === 'runnableByName' && !appInput.runnable.inlineScript}
|
||||
<span class="text-xs text-gray-500">
|
||||
Please configure the language in the inline script panel
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
ConnectedAppInput,
|
||||
RowAppInput,
|
||||
StaticAppInput,
|
||||
UserAppInput
|
||||
} from '$lib/components/apps/inputType'
|
||||
import type { AppViewerContext, InlineScript } from '$lib/components/apps/types'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
import TriggerBadgesList from './TriggerBadgesList.svelte'
|
||||
import { getDependencies } from './triggerListUtils'
|
||||
|
||||
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
|
||||
export let autoRefresh: boolean = false
|
||||
export let id: string
|
||||
export let inlineScript: InlineScript
|
||||
|
||||
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let addingDependency: boolean = false
|
||||
|
||||
function applyConnection() {
|
||||
if (!$connectingInput.opened && $connectingInput.input !== undefined && addingDependency) {
|
||||
if ($connectingInput.input.connection) {
|
||||
const x = {
|
||||
id: $connectingInput.input.connection.componentId,
|
||||
key: $connectingInput.input.connection.path
|
||||
}
|
||||
|
||||
if (!inlineScript) {
|
||||
return
|
||||
}
|
||||
|
||||
if (inlineScript.refreshOn?.find((y) => y.id === x.id && y.key === x.key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!inlineScript.refreshOn) {
|
||||
inlineScript.refreshOn = [x]
|
||||
} else {
|
||||
inlineScript.refreshOn.push(x)
|
||||
}
|
||||
|
||||
inlineScript = JSON.parse(JSON.stringify(inlineScript))
|
||||
|
||||
addingDependency = false
|
||||
}
|
||||
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
input: undefined,
|
||||
hoveredComponent: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: $connectingInput && applyConnection()
|
||||
</script>
|
||||
|
||||
<TriggerBadgesList
|
||||
{id}
|
||||
inputDependencies={getDependencies(fields)}
|
||||
frontendDependencies={inlineScript?.language === 'frontend'
|
||||
? inlineScript?.refreshOn?.map((x) => `${x.id} - ${x.key}`) ?? undefined
|
||||
: undefined}
|
||||
onLoad={autoRefresh}
|
||||
on:delete={(e) => {
|
||||
const index = e.detail.index
|
||||
if (inlineScript) {
|
||||
inlineScript.refreshOn?.splice(index, 1)
|
||||
}
|
||||
|
||||
inlineScript = JSON.parse(JSON.stringify(inlineScript))
|
||||
}}
|
||||
>
|
||||
{#if inlineScript?.language === 'frontend'}
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
color="light"
|
||||
btnClasses="!px-1 !py-0.5"
|
||||
on:click={() => {
|
||||
addingDependency = true
|
||||
$connectingInput = {
|
||||
opened: true,
|
||||
input: undefined,
|
||||
hoveredComponent: undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
Add dependency
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
</TriggerBadgesList>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
ConnectedAppInput,
|
||||
RowAppInput,
|
||||
StaticAppInput,
|
||||
UserAppInput
|
||||
} from '$lib/components/apps/inputType'
|
||||
import type { AppViewerContext } from '$lib/components/apps/types'
|
||||
import { getContext } from 'svelte'
|
||||
import { findGridItem } from '../../appUtils'
|
||||
import TriggerBadgesList from './TriggerBadgesList.svelte'
|
||||
import { getDependencies } from './triggerListUtils'
|
||||
|
||||
const { selectedComponent, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
|
||||
|
||||
$: gridItem = $selectedComponent ? findGridItem($app, $selectedComponent) : undefined
|
||||
|
||||
$: onClick =
|
||||
gridItem?.data.type &&
|
||||
['buttoncomponent', 'formbuttoncomponent', 'formcomponent'].includes(gridItem?.data.type)
|
||||
</script>
|
||||
|
||||
<TriggerBadgesList
|
||||
inputDependencies={getDependencies(fields)}
|
||||
onLoad={!onClick}
|
||||
id={$selectedComponent}
|
||||
{onClick}
|
||||
/>
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import type { AppViewerContext } from '$lib/components/apps/types'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { X } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { getAllRecomputeIdsForComponent } from '../../appUtils'
|
||||
|
||||
export let inputDependencies: string[] = []
|
||||
export let frontendDependencies: string[] | undefined = undefined
|
||||
export let onClick: boolean = false
|
||||
export let onLoad: boolean = false
|
||||
export let id: string | undefined = undefined
|
||||
|
||||
const colors = {
|
||||
red: 'text-red-800 border-red-600 bg-red-100',
|
||||
green: 'text-green-800 border-green-600 bg-green-100',
|
||||
indigo: 'text-indigo-800 border-indigo-600 bg-indigo-100',
|
||||
blue: 'text-blue-800 border-blue-600 bg-blue-100'
|
||||
}
|
||||
|
||||
let badgeClass = 'inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border'
|
||||
|
||||
const recomputedBadges: string[] = []
|
||||
const { app, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: if ($app && $selectedComponent && id) {
|
||||
const recomputeIds = getAllRecomputeIdsForComponent($app, id)
|
||||
|
||||
if ($selectedComponent && recomputeIds) {
|
||||
recomputeIds.forEach((x) => {
|
||||
recomputedBadges.push(x)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col items-start gap-2 mt-2 mb-1">
|
||||
{#if recomputedBadges.length === 0 && !onLoad && !onClick && inputDependencies?.length === 0 && !frontendDependencies}
|
||||
<p class="text-xs font-semibold text-slate-800 ">
|
||||
This script has no triggers. It will never run.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="text-sm font-semibold text-slate-800 ">Triggered by</div>
|
||||
|
||||
{#if onLoad || onClick}
|
||||
<div class="w-full">
|
||||
<div class="text-xs font-semibold text-slate-800 mb-1">Event</div>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#if onLoad}
|
||||
<span class={classNames(badgeClass, colors['green'])}>On load</span>
|
||||
{/if}
|
||||
{#if onClick}
|
||||
<span class={classNames(badgeClass, colors['green'])}>On click</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if inputDependencies.length > 0}
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="text-xs font-semibold text-slate-800 mb-1">Values change </div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each inputDependencies as label, index}
|
||||
<span class={classNames(badgeClass, colors['blue'])}>
|
||||
{label}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if recomputedBadges?.length > 0}
|
||||
<div class="w-full">
|
||||
<div class="text-xs font-semibold text-slate-800 mb-1">Success of</div>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each recomputedBadges as badge}
|
||||
<span class={classNames(badgeClass, colors['indigo'])}>{badge}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if frontendDependencies}
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="text-xs font-semibold text-slate-800 mb-1">Values change (Frontend)</div>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each frontendDependencies as label, index}
|
||||
<span class={classNames(badgeClass, colors['red'])}>
|
||||
{label}
|
||||
<button
|
||||
on:click={() => dispatch('delete', { index })}
|
||||
class="bg-red-300 cursor-pointer hover:bg-red-400 ml-1 rounded-md"
|
||||
>
|
||||
<X size={18} class="p-0.5" />
|
||||
</button>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import type {
|
||||
StaticAppInput,
|
||||
ConnectedAppInput,
|
||||
RowAppInput,
|
||||
UserAppInput
|
||||
} from '$lib/components/apps/inputType'
|
||||
|
||||
export function getDependencies(
|
||||
fields:
|
||||
| Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
|
||||
| undefined
|
||||
): string[] {
|
||||
let dependencies: string[] = []
|
||||
|
||||
if (!fields) return dependencies
|
||||
|
||||
Object.values(fields).forEach((field) => {
|
||||
if (field.type === 'connected' && dependencies && field.connection) {
|
||||
dependencies.push(`${field.connection.componentId} - ${field.connection.path}`)
|
||||
}
|
||||
})
|
||||
return dependencies
|
||||
}
|
||||
Reference in New Issue
Block a user