mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
feat: introduce container groups
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { GroupContext } from '../types'
|
||||
|
||||
export let groupContext: Writable<Record<string, any>>
|
||||
|
||||
setContext<GroupContext>('GroupContext', groupContext)
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
@@ -7,7 +7,7 @@
|
||||
TemplateV2Input,
|
||||
UploadAppInput
|
||||
} from '../../inputType'
|
||||
import type { AppViewerContext, ListContext, RichConfiguration } from '../../types'
|
||||
import type { AppViewerContext, GroupContext, ListContext, RichConfiguration } from '../../types'
|
||||
import { accessPropertyByPath } from '../../utils'
|
||||
import { computeGlobalContext, eval_like } from './eval'
|
||||
import deepEqualWithOrderedArray from './deepEqualWithOrderedArray'
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
const iterContext = getContext<ListContext>('ListWrapperContext')
|
||||
const rowContext = getContext<ListContext>('RowWrapperContext')
|
||||
const groupContext = getContext<GroupContext>('GroupContext')
|
||||
|
||||
let previousConnectedValue: any | undefined = undefined
|
||||
|
||||
@@ -33,19 +34,28 @@
|
||||
|
||||
$: fullContext = {
|
||||
iter: iterContext ? $iterContext : undefined,
|
||||
row: rowContext ? $rowContext : undefined
|
||||
row: rowContext ? $rowContext : undefined,
|
||||
group: groupContext ? $groupContext : undefined
|
||||
}
|
||||
|
||||
$: lastInput?.type == 'evalv2' &&
|
||||
(fullContext.iter != undefined || fullContext.row != undefined) &&
|
||||
lastInput.connections.some((x) => x.componentId == 'row' || x.componentId == 'iter') &&
|
||||
(fullContext.iter != undefined ||
|
||||
fullContext.row != undefined ||
|
||||
fullContext.group != undefined) &&
|
||||
lastInput.connections.some(
|
||||
(x) => x.componentId == 'row' || x.componentId == 'iter' || x.componentId == 'group'
|
||||
) &&
|
||||
debounceEval()
|
||||
|
||||
$: lastInput &&
|
||||
lastInput.type == 'templatev2' &&
|
||||
isCodeInjection(lastInput.eval) &&
|
||||
(fullContext.iter != undefined || fullContext.row != undefined) &&
|
||||
lastInput.connections.some((x) => x.componentId == 'row' || x.componentId == 'iter') &&
|
||||
(fullContext.iter != undefined ||
|
||||
fullContext.row != undefined ||
|
||||
fullContext.group != undefined) &&
|
||||
lastInput.connections.some(
|
||||
(x) => x.componentId == 'row' || x.componentId == 'iter' || x.componentId == 'group'
|
||||
) &&
|
||||
debounceTemplate()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
||||
import type { AppInputs, Runnable } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppViewerContext, CancelablePromise, InlineScript, ListContext } from '../../types'
|
||||
import type {
|
||||
AppViewerContext,
|
||||
CancelablePromise,
|
||||
GroupContext,
|
||||
InlineScript,
|
||||
ListContext
|
||||
} from '../../types'
|
||||
import { computeGlobalContext, eval_like } from './eval'
|
||||
import InputValue from './InputValue.svelte'
|
||||
import RefreshButton from './RefreshButton.svelte'
|
||||
@@ -65,6 +71,7 @@
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
const iterContext = getContext<ListContext>('ListWrapperContext')
|
||||
const rowContext = getContext<ListContext>('RowWrapperContext')
|
||||
const groupContext = getContext<GroupContext>('GroupContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -179,7 +186,8 @@
|
||||
runnable.inlineScript?.content,
|
||||
computeGlobalContext($worldStore, {
|
||||
iter: iterContext ? $iterContext : undefined,
|
||||
row: rowContext ? $rowContext : undefined
|
||||
row: rowContext ? $rowContext : undefined,
|
||||
group: groupContext ? $groupContext : undefined
|
||||
}),
|
||||
false,
|
||||
$state,
|
||||
|
||||
@@ -2,20 +2,28 @@
|
||||
import { getContext } from 'svelte'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
// import type { EvalV2AppInput, StaticAppInput } from '../../inputType'
|
||||
import { writable } from 'svelte/store'
|
||||
import { InputValue } from '../helpers'
|
||||
import GroupWrapper from '../GroupWrapper.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentContainerHeight: number
|
||||
export let customCss: ComponentCustomCSS<'containercomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let groupFields: RichConfigurations | undefined = undefined
|
||||
|
||||
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
//used so that we can count number of outputs setup for first refresh
|
||||
initOutput($worldStore, id, {})
|
||||
let groupContext = writable({})
|
||||
|
||||
let outputs = initOutput($worldStore, id, { group: $groupContext })
|
||||
|
||||
$: outputs.group.set($groupContext, true)
|
||||
|
||||
function onFocus() {
|
||||
$focusedGrid = {
|
||||
@@ -29,21 +37,29 @@
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
{#each Object.keys(groupFields ?? {}) as field}
|
||||
{#if groupFields && field in groupFields}
|
||||
<InputValue key={field} {id} input={groupFields[field]} bind:value={$groupContext[field]} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<div class="w-full h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
<SubGridEditor
|
||||
visible={render}
|
||||
{id}
|
||||
class={css?.container?.class}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
<GroupWrapper {groupContext}>
|
||||
<SubGridEditor
|
||||
visible={render}
|
||||
{id}
|
||||
class={css?.container?.class}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
</GroupWrapper>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
getRecommendedDimensionsByComponent,
|
||||
type AppComponent,
|
||||
type BaseComponent,
|
||||
type InitialAppComponent
|
||||
type InitialAppComponent,
|
||||
type TypedComponent
|
||||
} from './component'
|
||||
import { gridColumns } from '../gridUtils'
|
||||
import { allItems } from '../utils'
|
||||
@@ -598,7 +599,8 @@ export function sortGridItemsPosition<T>(
|
||||
export function connectInput(
|
||||
connectingInput: ConnectingInput,
|
||||
componentId: string,
|
||||
path: string
|
||||
path: string,
|
||||
componentType?: TypedComponent['type']
|
||||
): ConnectingInput {
|
||||
if (connectingInput) {
|
||||
if (connectingInput.onConnect) {
|
||||
@@ -610,7 +612,8 @@ export function connectInput(
|
||||
input: {
|
||||
connection: {
|
||||
componentId,
|
||||
path
|
||||
path,
|
||||
componentType
|
||||
},
|
||||
type: 'connected'
|
||||
},
|
||||
|
||||
@@ -15,7 +15,10 @@ export async function inferDeps(
|
||||
const noutputs = outputs
|
||||
.filter(
|
||||
([componentId, id]) =>
|
||||
componentId == 'row' || componentId == 'iter' || id in (worldOutputs[componentId] ?? {})
|
||||
componentId == 'row' ||
|
||||
componentId == 'iter' ||
|
||||
componentId == 'group' ||
|
||||
id in (worldOutputs[componentId] ?? {})
|
||||
)
|
||||
.map(([componentId, id]) => ({
|
||||
componentId: componentId,
|
||||
|
||||
@@ -480,6 +480,7 @@
|
||||
/>
|
||||
{:else if component.type === 'containercomponent'}
|
||||
<AppContainer
|
||||
groupFields={component.groupFields}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
|
||||
@@ -48,6 +48,7 @@ import type {
|
||||
ComponentCustomCSS,
|
||||
GridItem,
|
||||
RichConfiguration,
|
||||
RichConfigurations,
|
||||
StaticRichConfigurations
|
||||
} from '../../types'
|
||||
import type { Size } from '../../svelte-grid/types'
|
||||
@@ -115,7 +116,9 @@ export type TabsComponent = BaseComponent<'tabscomponent'> & {
|
||||
disabledTabs: RichConfiguration[]
|
||||
}
|
||||
export type ListComponent = BaseComponent<'listcomponent'>
|
||||
export type ContainerComponent = BaseComponent<'containercomponent'>
|
||||
export type ContainerComponent = BaseComponent<'containercomponent'> & {
|
||||
groupFields: RichConfigurations
|
||||
}
|
||||
export type DrawerComponent = BaseComponent<'drawercomponent'>
|
||||
export type MapComponent = BaseComponent<'mapcomponent'>
|
||||
export type VerticalSplitPanesComponent = BaseComponent<'verticalsplitpanescomponent'> & {
|
||||
|
||||
@@ -39,7 +39,11 @@
|
||||
componentId={gridItem.id}
|
||||
on:select={({ detail }) => {
|
||||
if ($connectingInput.opened) {
|
||||
$connectingInput = connectInput($connectingInput, gridItem.id, detail)
|
||||
let typ = gridItem?.data?.type
|
||||
let splitted = detail?.split('.')
|
||||
let componentId = typ == 'containercomponent' ? splitted?.[0] : gridItem.id
|
||||
let path = typ == 'containercomponent' ? splitted?.[1] : detail
|
||||
$connectingInput = connectInput($connectingInput, componentId, path, typ)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
if (observableOutputs) {
|
||||
Object.entries(observableOutputs).forEach(([k, output]) => {
|
||||
object[k] = undefined
|
||||
console.log('subscribing to', k, output)
|
||||
output?.subscribe(
|
||||
{
|
||||
id: 'alloutputs' + componentId + '-' + k,
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import { inferDeps } from '../appUtilsInfer'
|
||||
import EvalV2InputEditor from './inputEditor/EvalV2InputEditor.svelte'
|
||||
import type { ResultAppInput } from '../../inputType'
|
||||
import GridGroup from './GridGroup.svelte'
|
||||
|
||||
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
|
||||
undefined
|
||||
@@ -310,6 +311,11 @@
|
||||
bind:component={componentSettings.item.data}
|
||||
word="Step"
|
||||
/>
|
||||
{:else if componentSettings.item.data.type === 'containercomponent'}
|
||||
<GridGroup
|
||||
bind:groupFields={componentSettings.item.data.groupFields}
|
||||
bind:component={componentSettings.item.data}
|
||||
/>
|
||||
{:else if componentSettings.item.data.type === 'conditionalwrapper'}
|
||||
<GridCondition
|
||||
bind:conditions={componentSettings.item.data.conditions}
|
||||
@@ -333,7 +339,7 @@
|
||||
userInputEnabled={false}
|
||||
/>
|
||||
</PanelSection>
|
||||
{:else}
|
||||
{:else if componentSettings.item.data.type != 'containercomponent'}
|
||||
<div class="h-full w-full text-sm text-tertiary text-center py-8 px-2"
|
||||
>{ccomponents[component.type].name} has no configuration</div
|
||||
>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { RichConfigurations } from '../../types'
|
||||
import type { AppComponent } from '../component'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
export let groupFields: RichConfigurations | undefined
|
||||
|
||||
export let component: AppComponent
|
||||
|
||||
// const { app, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let fieldName: string = ''
|
||||
function addField(name: string) {
|
||||
if (name == '') return
|
||||
groupFields = {
|
||||
...(groupFields ?? {}),
|
||||
[name]: {
|
||||
type: 'static',
|
||||
value: '',
|
||||
fieldType: 'object'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex p-2 gap-1 items-center">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={groupFields != undefined}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
groupFields = {}
|
||||
} else {
|
||||
groupFields = undefined
|
||||
}
|
||||
console.log(groupFields)
|
||||
}}
|
||||
options={{ right: 'container is a component group' }}
|
||||
/>
|
||||
<Tooltip
|
||||
>Group fields allow inner components to depend on the group fields which make the container a
|
||||
group of component that is encapsulated. Inside the group, it is possible to retrieve the values
|
||||
using `group.<x />` where x is the group field name</Tooltip
|
||||
>
|
||||
</div>
|
||||
{#if groupFields != undefined}
|
||||
<PanelSection
|
||||
title={`Group Fields ${
|
||||
Object.keys(groupFields ?? {}).length > 0
|
||||
? `(${Object.keys(groupFields ?? {}).length ?? 0})`
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{#if Object.keys(groupFields ?? {}).length == 0}
|
||||
<span class="text-xs text-tertiary">No group fields</span>
|
||||
{/if}
|
||||
<div class="w-full flex gap-2 flex-col mt-2">
|
||||
<InputsSpecsEditor
|
||||
on:delete={(e) => {
|
||||
if (!groupFields) {
|
||||
return
|
||||
}
|
||||
delete groupFields[e.detail]
|
||||
groupFields = groupFields
|
||||
}}
|
||||
id={component.id}
|
||||
shouldCapitalize={false}
|
||||
displayType
|
||||
deletable
|
||||
bind:inputSpecs={groupFields}
|
||||
/>
|
||||
<div class="flex flex-row gap-2 items-center relative">
|
||||
<input
|
||||
type="text"
|
||||
on:keydown|stopPropagation={(event) => {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
addField(fieldName)
|
||||
break
|
||||
}
|
||||
}}
|
||||
placeholder="Group Field Name"
|
||||
bind:value={fieldName}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={fieldName == ''}
|
||||
size="sm"
|
||||
color="light"
|
||||
variant="border"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => addField(fieldName)}
|
||||
iconOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSection>
|
||||
{/if}
|
||||
@@ -17,15 +17,6 @@
|
||||
|
||||
export let canDisableTabs: boolean = false
|
||||
|
||||
// Migration code
|
||||
$: if (tabs?.length !== disabledTabs?.length && canDisableTabs) {
|
||||
disabledTabs = Array(tabs.length).fill({
|
||||
type: 'static',
|
||||
value: false,
|
||||
fieldType: 'boolean'
|
||||
})
|
||||
}
|
||||
|
||||
export let word: string = 'Tab'
|
||||
|
||||
export let component: AppComponent
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
{#if displayType}
|
||||
<div class="text-xs text-tertiary">
|
||||
<div class="text-xs text-tertiary mr-1">
|
||||
{fieldType === 'array' && subFieldType
|
||||
? `${fieldTypeToTsType(subFieldType)}[]`
|
||||
: fieldTypeToTsType(fieldType)}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import CloseButton from '$lib/components/common/CloseButton.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { RichConfigurations } from '../../types'
|
||||
|
||||
import InputsSpecEditor from './InputsSpecEditor.svelte'
|
||||
@@ -11,8 +13,11 @@
|
||||
export let shouldCapitalize: boolean = true
|
||||
export let resourceOnly = false
|
||||
export let displayType = false
|
||||
export let deletable = false
|
||||
|
||||
$: finalInputSpecsConfiguration = inputSpecsConfiguration ?? inputSpecs
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if inputSpecs}
|
||||
@@ -48,6 +53,10 @@
|
||||
customTitle={meta?.['customTitle']}
|
||||
{displayType}
|
||||
/>
|
||||
{#if deletable}
|
||||
<div class="flex flex-row-reverse -mt-4">
|
||||
<CloseButton noBg on:close={() => dispatch('delete', k)} />
|
||||
</div>{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReadFileAs } from '../common/fileInput/model'
|
||||
import type { TypedComponent } from './editor/component'
|
||||
import type { InlineScript } from './types'
|
||||
|
||||
export type InputType =
|
||||
@@ -25,6 +26,7 @@ export type InputType =
|
||||
// Connection to an output of another component
|
||||
// defined by the id of the component and the path of the output
|
||||
export type InputConnection = {
|
||||
componentType?: TypedComponent['type']
|
||||
componentId: string
|
||||
path: string
|
||||
}
|
||||
|
||||
@@ -163,6 +163,8 @@ export type ListContext = Writable<{
|
||||
|
||||
export type ListInputs = (id: string, value: any) => void
|
||||
|
||||
export type GroupContext = Writable<Record<string, any>>
|
||||
|
||||
export type AppViewerContext = {
|
||||
worldStore: Writable<World>
|
||||
app: Writable<App>
|
||||
|
||||
@@ -185,7 +185,9 @@ declare function closeModal(id: string): void;
|
||||
}
|
||||
declare const state: ${JSON.stringify(state)};
|
||||
declare const iter: {index: number, value: any};
|
||||
declare const row: {index: number, value: any};
|
||||
declare const row: Record<string, any>;
|
||||
declare const group: Record<string, any>;
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user