Files
windmill/frontend/src/lib/components/apps/editor/ComponentHeader.svelte
T

299 lines
9.4 KiB
Svelte

<script lang="ts">
import { classNames } from '$lib/utils'
import type { AppViewerContext } from '../types'
import { Anchor, ArrowDownFromLine, Bug, Expand, Network, Pen, Plug } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import Popover from '$lib/components/Popover.svelte'
import { Button, Popup } from '$lib/components/common'
import type { AppComponent } from './component'
import { twMerge } from 'tailwind-merge'
import { connectOutput } from './appUtils'
import TabsDebug from './TabsDebug.svelte'
import ComponentOutputViewer from './contextPanel/ComponentOutputViewer.svelte'
import DecisionTreeDebug from './DecisionTreeDebug.svelte'
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
export let component: AppComponent
export let selected: boolean
export let locked: boolean = false
export let hover: boolean = false
export let connecting: boolean = false
export let hasInlineEditor: boolean = false
export let inlineEditorOpened: boolean = false
export let errorHandledByComponent: boolean = false
export let fullHeight: boolean = false
export let componentContainerWidth: number
//export let willNotDisplay: boolean = false
const DECISION_TREE_THRESHOLD = 300
const STEPPER_THRESHOLD = 180
const CONDITIONAL_WRAPPER_THRESHOLD = 200
const MINIMUM_WIDTH = 20
let maxWidth = 10
let isManuallySelected = false
let componentIsDebugging = false
let id_width = 0
$: maxWidth = Math.max(Math.round(0.2 * componentContainerWidth), MINIMUM_WIDTH)
const dispatch = createEventDispatcher()
const { errorByComponent, openDebugRun, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
// Function to check if any condition is met
function checkComponentOptions(): boolean {
const componentOptions = {
isEditable: hasInlineEditor,
isConditionalWrapper: component.type === 'conditionalwrapper',
hasTabs:
component.type === 'steppercomponent' ||
(component.type === 'tabscomponent' &&
component.configuration.tabsKind.type === 'static' &&
component.configuration.tabsKind.value === 'invisibleOnView'),
isDecisionTree: component.type === 'decisiontreecomponent'
}
return Object.values(componentOptions).some((value) => value)
}
let connectingPopupHover = false
$: connectingPopupHover && dispatch('mouseover')
let hoverHeader = false
</script>
{#if connecting}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<div
class="absolute z-50 overflow-auto -top-[18px]"
style="left: {id_width}px;"
data-connection-button
>
<Popup
floatingConfig={{ strategy: 'fixed', placement: 'bottom-start' }}
bind:popupHover={connectingPopupHover}
>
<svelte:fragment slot="button">
<AnimatedButton
animate={true}
baseRadius="9999px"
wrapperClasses="h-full w-full pt-2"
marginWidth="2px"
animationDuration="3s"
>
<div
id={`connect-output-${component.id}`}
class="h-[20px] w-[20px] bg-surface rounded-full center-center text-primary"
title="Outputs"
aria-label="Open output"><Plug size={12} /></div
>
</AnimatedButton>
</svelte:fragment>
<ComponentOutputViewer
suffix="connect"
on:select={({ detail }) =>
connectOutput(connectingInput, component.type, component.id, detail)}
componentId={component.id}
/>
</Popup>
</div>
{/if}
{#if selected || hover}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<div class="-top-[18px] -left-[8px] flex flex-row flex-nowrap w-fit h-fit absolute gap-0.5">
<div
on:mouseover|stopPropagation={() => {
hoverHeader = true
dispatch('mouseover')
}}
on:mouseleave|stopPropagation={() => {
hoverHeader = false
}}
on:mousedown|stopPropagation|capture
draggable="false"
title={`Id: ${component.id}`}
class={twMerge(
'py-0.5 text-2xs w-fit h-full min-h-5 rounded z-50 cursor-move flex flex-row flex-nowrap font-semibold items-center shadow',
selected
? 'bg-blue-600/90 text-white'
: $connectingInput.opened
? 'bg-[#f8aa4b]/90 text-white'
: 'bg-blue-400/90 text-white'
)}
>
<div
class={`px-1 text-2xs w-full min-w-4 h-full truncate`}
style="max-width: {maxWidth}px;"
bind:clientWidth={id_width}
>
{component.id}
</div>
{#if !connecting}
<div class="flex flex-row px-1 py-0 gap-0.5">
<button
title="Fill height"
class={twMerge(
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
fullHeight
? 'bg-blue-300 text-blue-800'
: 'text-white hover:bg-blue-400 hover:text-white'
)}
on:click={() => dispatch('fillHeight')}
on:pointerdown|stopPropagation
>
<ArrowDownFromLine aria-label="Full height" size={11} />
</button>
<button
title="Lock Position"
class={twMerge(
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
locked ? 'bg-blue-300 text-blue-800' : 'text-white hover:bg-blue-400 hover:text-white'
)}
on:click={() => dispatch('lock')}
on:pointerdown|stopPropagation
>
{#if locked}
<Anchor aria-label="Unlock position" size={11} />
{:else}
<Anchor aria-label="Lock position" size={11} />
{/if}
</button>
{#if hoverHeader}
<button
title="Expand"
class={twMerge(
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full text-white hover:bg-blue-400 hover:text-white'
)}
on:click={() => dispatch('expand')}
on:pointerdown|stopPropagation
>
<Expand aria-label="Expand" size={11} />
</button>
{/if}
</div>
{/if}
</div>
{#if selected && !connecting && checkComponentOptions()}
<div
class={twMerge(
'px-1 py-0.5 text-2xs font-semibold w-fit min-h-5 shadow rounded z-50 flex flex-row items-center flex-nowrap',
isManuallySelected || componentIsDebugging
? 'bg-red-100 text-red-600 border-red-500'
: 'bg-blue-100/90 border-blue-200 text-blue-600'
)}
>
{#if hasInlineEditor}
<button
title="Edit"
class={twMerge(
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
inlineEditorOpened
? 'bg-blue-300 text-blue-800'
: 'text-blue-600 hover:bg-blue-300 hover:text-blue-800'
)}
on:click={() => dispatch('triggerInlineEditor')}
on:pointerdown|stopPropagation
>
<Pen aria-label="Edit" size={11} />
</button>
{/if}
{#if component.type === 'conditionalwrapper'}
<TabsDebug
id={component.id}
tabs={component.conditions ?? []}
isConditionalDebugMode
isSmall={componentContainerWidth < CONDITIONAL_WRAPPER_THRESHOLD}
bind:isManuallySelected
/>
{:else if component.type === 'steppercomponent' || (component.type === 'tabscomponent' && component.configuration.tabsKind.type === 'static' && component.configuration.tabsKind.value === 'invisibleOnView')}
<TabsDebug
id={component.id}
tabs={component.tabs ?? []}
isSmall={componentContainerWidth < STEPPER_THRESHOLD}
bind:isManuallySelected
/>
{:else if component.type === 'decisiontreecomponent'}
<button
title={'Open Decision Tree Editor'}
class={twMerge(
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full center-center',
componentIsDebugging
? 'text-red-600 hover:bg-red-300 hover:text-red-800'
: 'text-blue-600 hover:bg-blue-300 hover:text-blue-800'
)}
on:click={() => {
const element = document.getElementById(`decision-tree-graph-editor`)
if (element) {
element.click()
}
}}
on:pointerdown|stopPropagation
>
<Network size={11} />
</button>
<DecisionTreeDebug
id={component.id}
nodes={component.nodes ?? []}
isSmall={componentContainerWidth < DECISION_TREE_THRESHOLD}
bind:componentIsDebugging
/>
{/if}
<!-- {#if willNotDisplay}
<Popover>
<svelte:fragment slot="text">
This component won't render, because an other component above it is set to fill the
height.
</svelte:fragment>
<div
title="Fill height"
class={classNames(
'px-1 text-2xs py-0.5 font-bold w-fit border cursor-pointer rounded-sm',
'bg-red-100 text-red-600 border-red-500 hover:bg-red-200 hover:text-red-800'
)}
>
<EyeOff aria-label="Expand position" size={14} />
</div>
</Popover>
{/if} -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
</div>
{/if}
</div>
{/if}
{#if !errorHandledByComponent && $errorByComponent[component.id]}
{@const error = $errorByComponent[component.id]?.error}
<span
title="Error"
class={classNames(
'text-red-500 px-1 text-2xs py-0.5 font-bold w-fit absolute border border-red-500 -bottom-1 shadow left-1/2 transform -translate-x-1/2 z-50 cursor-pointer'
)}
>
<Popover notClickable placement="bottom" popupClass="!bg-surface border w-96">
<Bug size={14} />
<span slot="text">
<div class="bg-surface">
<pre class=" whitespace-pre-wrap text-red-600 bg-surface border w-full p-4 text-xs mb-2"
>{error ?? ''}
</pre>
</div>
<Button
color="red"
variant="border"
on:click={() => $openDebugRun?.($errorByComponent[component.id]?.id ?? '')}
>Open Debug Runs</Button
>
</span>
</Popover>
</span>
{/if}