feat: UX improvements (all inputs)

* Monaco transparent bg

* started improving input transform form

* always show static/f selector

* fix connecting btn changing size

* pretty Fill Inputs button

* ResizeTransitionWrapper

* Prettier TemplateEditor

* prevent double onpointerdown when clicking button to close

* text hint

* force focus border for TemplateEditor

* styling in js mode

* update select style

* fix jittery fake monaco placeholder

* select nits

* aiproviderpicker + nits

* smaller ${...} badge

* no-default-style

* select dropdown slide

* nit

* Refresh button in flow picker quick

* jsonEditor pretty

* ai provider toggle button more

* change resource edit button pos

* ResourcePicker Add and Refresh btn

* fix scrollbar

* Fix FileInput and S3 Arg Input

* fix textarea styling

* nicer refresh button in Test This Step

* fix togglebutton border in darkmode

* rounded nit

* Fix multiselect styling

* Prevent crash when selecting dyn-multiselect

* missing $derived and $state => reactivity issue when switching between DynSelect and DynMultiselect

* forgot $effect.pre

* fix nested objects

* nits

* prettier json toggle and array inputs

* array input nits

* nit

* fix json toggle appearing in fileinputs

* nit

* started updating PropertyEditor

* (stash) fix select dropdown animation teleporting from bottom to top

* nit

* nit

* resize transition in schemaform

* nit

* nit typo

* nit enableFlyTransition

* shadow nit

* small consistency changes

* user setting nit

* resize transition in module preview form

* more space

* nit readability on hover

* DateTimeInput new style

* nit fix

* remove yPadding in template and simple editor

* nits

* Revert "remove yPadding in template and simple editor"

This reverts commit 8f27c8d0b8.

* nit

* Fix proppicker border

* fix inconsistent spacing btw arginput and input transform form field headers

* consistent add item button

* nit

* s3 settings nits

* RunsFilter fix

* gray ${...} badge

* border fix darkmode

* nit

* nit app editor consistency

* fix step input gen style

* nit fix

* nits

* toggle border

* nit toggle button

* nit font-medium

* nit font-medium

* nit font-medium

* nit font-medium

* nit

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Diego Imbert
2025-09-27 23:52:13 +02:00
committed by GitHub
parent 8417001956
commit c394a47aba
68 changed files with 1378 additions and 949 deletions
@@ -8,6 +8,7 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import ToggleButtonMore from './common/toggleButton-v2/ToggleButtonMore.svelte'
interface ProviderValue {
kind?: AIProvider
@@ -154,22 +155,36 @@
}
</script>
<div class="w-full flex flex-col gap-3">
<div
class="w-full flex flex-col gap-1 bg-surface-secondary border-[1px] border-nord-400 dark:border-nord-300 rounded-md"
>
<!-- Provider Selection -->
<div class="flex flex-col gap-2">
<ToggleButtonGroup selected={value?.kind} onSelected={onProviderChange} {disabled} wrap>
<div class="flex flex-col gap-2 m-[-1px] mt-[-1px]">
<ToggleButtonGroup
selected={value?.kind}
onSelected={onProviderChange}
{disabled}
wrap
tabListClass="w-full bg-transparent"
>
{#snippet children({ item })}
{#each providerOptions as option}
<ToggleButton value={option.value} label={option.label} {item} />
{#each providerOptions.slice(0, 3) as option}
<ToggleButton value={option.value} label={option.label} {item} class="bg-transparent" />
{/each}
<ToggleButtonMore
class="ml-auto"
togglableItems={providerOptions.slice(3)}
{item}
bind:selected={() => value?.kind, (v) => v && onProviderChange(v)}
/>
{/snippet}
</ToggleButtonGroup>
</div>
<!-- Resource Selection -->
<div class="flex flex-col border border-gray-200 rounded-md p-2 gap-2">
<div class="flex flex-col rounded-md p-2 gap-2">
<div class="flex flex-col gap-1">
<p class="text-sm font-medium text-primary">resource</p>
<p class="text-sm font-normal text-tertiary">resource</p>
<ResourcePicker
bind:value={
() => resourceValueToPath(value?.resource),
@@ -183,12 +198,13 @@
disabled={disabled || !value?.kind}
placeholder="Select resource"
selectFirst={true}
selectInputClass="!bg-surface"
/>
</div>
<!-- Model Selection -->
<div class="flex flex-col gap-1">
<p class="text-sm font-medium text-primary">model</p>
<p class="text-sm font-normal text-tertiary">model</p>
<Select
{items}
bind:value={value.model}
@@ -203,6 +219,7 @@
clearable={false}
noItemsMsg={'No models available'}
bind:filterText
inputClass="min-h-10 !bg-surface disabled:!bg-surface-disabled"
/>
</div>
</div>
+5 -2
View File
@@ -12,6 +12,7 @@
valid: boolean
create: boolean
enumLabels?: Record<string, string> | undefined
selectClass?: string
}
let {
@@ -22,7 +23,8 @@
defaultValue,
valid,
create,
enumLabels = undefined
enumLabels = undefined,
selectClass = ''
}: Props = $props()
const dispatch = createEventDispatcher()
@@ -54,7 +56,7 @@
<div class="w-full flex-col">
<div class="w-full">
<Select
inputClass={valid ? '' : '!border-red-500/60'}
error={!valid}
clearable
{disabled}
autofocus={autofocus ?? undefined}
@@ -70,6 +72,7 @@
}
onFocus={() => dispatch('focus')}
onBlur={() => dispatch('blur')}
inputClass={selectClass}
/>
</div>
</div>
+242 -174
View File
@@ -46,6 +46,7 @@
import { workspaceStore } from '$lib/stores'
import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte'
import AIProviderPicker from './AIProviderPicker.svelte'
import TextInput, { inputBaseClass, inputBorderClass } from './text_input/TextInput.svelte'
interface Props {
label?: string
@@ -71,6 +72,7 @@
properties?: { [name: string]: SchemaProperty }
}
| undefined
lightHeaderFont?: boolean
displayHeader?: boolean
properties?: { [name: string]: SchemaProperty } | undefined
nestedRequired?: string[] | undefined
@@ -120,6 +122,8 @@
| undefined
workspace?: string | undefined
actions?: import('svelte').Snippet
innerBottomSnippet?: import('svelte').Snippet
fieldHeaderActions?: import('svelte').Snippet
}
let {
@@ -133,7 +137,7 @@
oneOf = $bindable(undefined),
required = false,
pattern = $bindable(undefined),
valid = $bindable(undefined),
valid = $bindable(undefined), // Note : this should not exist, valid and error should be one coherent state
enum_ = $bindable(undefined),
disabled = false,
itemsType = $bindable(undefined),
@@ -176,7 +180,10 @@
appPath = undefined,
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
actions
actions,
innerBottomSnippet,
fieldHeaderActions,
lightHeaderFont = false
}: Props = $props()
$effect(() => {
@@ -468,6 +475,11 @@
let { debounced, clearDebounce } = debounce(() => compareValues(value), 50)
let inputCat = $derived(computeInputCat(type, format, itemsType?.type, enum_, contentEncoding))
let displayJsonToggleHeader = $derived(
displayHeader &&
inputCat === 'list' &&
!(itemsType?.resourceType === 's3_object' || itemsType?.resourceType === 's3object')
)
$effect(() => {
oneOf && untrack(() => updateOneOfSelected(oneOf))
})
@@ -518,10 +530,10 @@
{#snippet variableInput()}
{#if variableEditor}
<div class="text-sm text-tertiary">
<div class="text-sm text-hint">
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
Linked to variable <button
class="text-blue-500 underline"
class="text-nord-950 underline font-normal"
onclick={() => variableEditor?.editVariable?.(value.slice(5))}>{value.slice(5)}</button
>
{/if}
@@ -587,24 +599,43 @@
</div>
{/if}
{#if displayHeader}
<FieldHeader
prettify={prettifyHeader}
label={title && !emptyString(title) ? title : label}
{disabled}
{required}
{type}
{contentEncoding}
{format}
{simpleTooltip}
{lightHeader}
{displayType}
labelClass={css?.label?.class}
/>
{/if}
<div class="flex min-h-7 items-end pb-1">
<FieldHeader
prettify={prettifyHeader}
label={title && !emptyString(title) ? title : label}
{disabled}
{required}
{type}
{contentEncoding}
{format}
{simpleTooltip}
{lightHeader}
{displayType}
labelClass={twMerge(
lightHeaderFont ? '!font-normal !text-sm text-tertiary' : '',
css?.label?.class
)}
/>
<div class="ml-auto flex gap-2">
{#if displayJsonToggleHeader}
<Toggle
on:change={(e) => {
// Once the user has changed the input type, we should not change it back automatically
if (!hasIsListJsonChanged) {
hasIsListJsonChanged = true
}
{#if description}
<div class={twMerge('text-xs italic pb-1 text-secondary', css?.description?.class)}>
<pre class="font-main whitespace-normal">{description}</pre>
evalValueToRaw()
isListJson = !isListJson
}}
checked={isListJson}
textClass="text-secondary"
size="xs"
options={{ left: 'json' }}
/>
{/if}
{@render fieldHeaderActions?.()}
</div>
</div>
{/if}
@@ -629,7 +660,22 @@
/>
{:else}
<div class="relative w-full">
<input
<TextInput
inputProps={{
autofocus,
onfocus: bubble('focus'),
onblur: bubble('blur'),
disabled,
type: 'number',
onkeydown: () => (ignoreValueUndefined = true),
placeholder: placeholder ?? defaultValue ?? '',
min: extra['min'],
max: extra['max']
}}
{error}
bind:value
/>
<!-- <input
{autofocus}
onfocus={bubble('focus')}
onblur={bubble('blur')}
@@ -645,7 +691,7 @@
bind:value
min={extra['min']}
max={extra['max']}
/>
/> -->
</div>
{/if}
{:else if inputCat == 'boolean'}
@@ -679,6 +725,7 @@
bind:editor
{appPath}
{computeS3ForceViewerPolicies}
bottom={innerBottomSnippet}
/>
{:else if inputCat == 'object' && format == 'json-schema'}
{#await import('$lib/components/EditableSchemaForm.svelte')}
@@ -722,8 +769,9 @@
<Module.default code={JSON.stringify(value, null, 2)} bind:value />
{/await}
{:else}
<div class="py-4 pr-2 pl-6 border rounded-md w-full">
<div class="px-3 pt-6 border rounded-md w-full">
<SchemaForm
lightHeaderFont
{onlyMaskPassword}
{disablePortal}
{disabled}
@@ -768,14 +816,34 @@
/>
</div>
{:else}
<div class="w-full">
<div class="w-full flex flex-col gap-y-1">
{#key redraw}
{#if Array.isArray(value)}
{#each value ?? [] as v, i}
{#if i < itemsLimit}
<div class="flex max-w-md mt-1 w-full items-center">
<div class="flex w-full items-center relative">
{#snippet deleteItemBtn()}
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
aria-label="Clear"
onclick={() => {
value = value.filter((_, index) => index !== i)
redraw += 1
}}
>
<X size={14} />
</button>
{/snippet}
{#if itemsType?.type == 'number'}
<input type="number" bind:value={value[i]} id="arg-input-number-array" />
<TextInput
inputProps={{ type: 'number', id: 'arg-input-number-array' }}
class="pr-8"
bind:value={value[i]}
/>
<div class="absolute z-10 right-1.5">
{@render deleteItemBtn()}
</div>
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
<input
type="file"
@@ -783,6 +851,7 @@
onchange={(x) => fileChanged(x, (val) => (value[i] = val))}
multiple={false}
/>
{@render deleteItemBtn()}
{:else if itemsType?.type == 'object' && itemsType?.resourceType === undefined && itemsType?.properties === undefined && !(format?.startsWith('resource-') && resourceTypes?.includes(format.split('-')[1]))}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
@@ -792,6 +861,7 @@
bind:value={value[i]}
/>
{/await}
{@render deleteItemBtn()}
{:else if Array.isArray(itemsType?.enum)}
<ArgEnum
create={extra['disableCreate'] != true}
@@ -809,6 +879,7 @@
enum_={itemsType?.enum ?? []}
enumLabels={extra['enumLabels']}
/>
{@render deleteItemBtn()}
{:else if (itemsType?.type == 'resource' && itemsType?.resourceType && resourceTypes?.includes(itemsType.resourceType)) || (format?.startsWith('resource-') && resourceTypes?.includes(format.split('-')[1]))}
{@const resourceFormat =
itemsType?.type == 'resource' &&
@@ -821,6 +892,7 @@
format={resourceFormat}
defaultValue={undefined}
/>
{@render deleteItemBtn()}
{:else if itemsType?.type == 'resource'}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
@@ -837,9 +909,11 @@
bind:value={value[i]}
/>
{/await}
{@render deleteItemBtn()}
{:else if itemsType?.type === 'object' && itemsType?.properties}
<div class="p-8 border rounded-md w-full">
<SchemaForm
lightHeaderFont
{onlyMaskPassword}
{disablePortal}
{disabled}
@@ -848,20 +922,17 @@
bind:args={value[i]}
/>
</div>
{@render deleteItemBtn()}
{:else}
<input type="text" bind:value={value[i]} id="arg-input-array" />
<TextInput
inputProps={{ type: 'text', id: 'arg-input-array' }}
class="pr-8"
bind:value={value[i]}
/>
<div class="absolute z-10 right-1.5">
{@render deleteItemBtn()}
</div>
{/if}
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
aria-label="Clear"
onclick={() => {
value = value.filter((_, index) => index !== i)
redraw += 1
}}
>
<X size={14} />
</button>
</div>
{/if}
{/each}
@@ -874,7 +945,7 @@
{#if value.startsWith('$res:')}
{@render resourceInput()}
{:else}
<div class="text-red-500">
<div class="text-red-500 text-xs">
Invalid string value: "{value}", expected array. Click add item to turn it
into an array.
</div>
@@ -882,59 +953,58 @@
{/if}
{/key}
</div>
<div class="flex mt-2 gap-20 items-baseline">
<Button
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
if (itemsType?.type == 'number') {
value = value.concat(0)
} else if (
(itemsType?.type == 'object' &&
!(
format?.startsWith('resource-') &&
resourceTypes?.includes(format.split('-')[1])
)) ||
(itemsType?.type == 'resource' &&
!(
itemsType?.resourceType && resourceTypes?.includes(itemsType?.resourceType)
))
) {
value = value.concat({})
} else {
value = value.concat('')
}
}}
id="arg-input-add-item"
startIcon={{ icon: Plus }}
>
Add item
</Button>
</div>
<Button
variant="border"
color="light"
size="xs"
btnClasses="text-tertiary py-2.5"
wrapperClasses="w-full {Array.isArray(value) && value.length > 0 ? 'mt-1.5' : ''}"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
if (itemsType?.type == 'number') {
value = value.concat(0)
} else if (
(itemsType?.type == 'object' &&
!(
format?.startsWith('resource-') &&
resourceTypes?.includes(format.split('-')[1])
)) ||
(itemsType?.type == 'resource' &&
!(itemsType?.resourceType && resourceTypes?.includes(itemsType?.resourceType)))
) {
value = value.concat({})
} else {
value = value.concat('')
}
}}
id="arg-input-add-item"
startIcon={{ icon: Plus }}
>
Add item
</Button>
{/if}
</div>
<div class="mt-2 mr-4">
<Toggle
on:change={(e) => {
// Once the user has changed the input type, we should not change it back automatically
if (!hasIsListJsonChanged) {
hasIsListJsonChanged = true
}
{#if !displayHeader}
<div class="block mt-2.5 pl-2">
<Toggle
on:change={(e) => {
// Once the user has changed the input type, we should not change it back automatically
if (!hasIsListJsonChanged) {
hasIsListJsonChanged = true
}
evalValueToRaw()
isListJson = !isListJson
}}
checked={isListJson}
textClass="text-secondary"
size="xs"
options={{ right: 'json' }}
/>
</div>
evalValueToRaw()
isListJson = !isListJson
}}
checked={isListJson}
textClass="text-secondary"
size="xs"
options={{ left: 'json' }}
/>
</div>
{/if}
</div>
{:else if inputCat == 'dynamic'}
<DynamicInput name={label} {otherArgs} {helperScript} bind:value format={format ?? ''} />
@@ -957,7 +1027,7 @@
/>
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
{#if oneOf && oneOf.length >= 2}
<div class="flex flex-col gap-2 w-full">
<div class="flex flex-col gap-2 w-full border rounded-md p-2">
{#if oneOf && oneOf.length >= 2}
<ToggleButtonGroup
selected={oneOfSelected}
@@ -997,70 +1067,70 @@
{@const obj = oneOf[objIdx]}
{#if obj && obj.properties && Object.keys(obj.properties).length > 0}
{#key redraw}
<div class="py-4 pr-2 pl-6 border rounded w-full">
{#if orderEditable}
<SchemaFormDnd
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
bind:schema={
() => ({
properties: obj.properties ?? {},
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}),
() => {
dispatch('nestedChange')
}
}
bind:args={value}
hiddenArgs={[
oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
]}
on:reorder={(e) => {
if (oneOf && oneOf[objIdx]) {
const keys = e.detail
oneOf[objIdx].order = keys
}
}}
on:nestedChange
{shouldDispatchChanges}
/>
{:else}
<SchemaForm
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
hiddenArgs={['label', 'kind']}
schema={{
properties: obj.properties,
{#if orderEditable}
<SchemaFormDnd
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
bind:schema={
() => ({
properties: obj.properties ?? {},
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}}
bind:args={
() => value,
(v) => {
value = { ...v, [tagKey]: oneOfSelected }
}
}
{shouldDispatchChanges}
on:change={() => {
}),
() => {
dispatch('nestedChange')
}}
on:nestedChange
/>
{/if}
</div>
}
}
bind:args={value}
hiddenArgs={[
oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
]}
on:reorder={(e) => {
if (oneOf && oneOf[objIdx]) {
const keys = e.detail
oneOf[objIdx].order = keys
}
}}
on:nestedChange
{shouldDispatchChanges}
/>
{:else}
<SchemaForm
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
{disabled}
{prettifyHeader}
hiddenArgs={['label', 'kind']}
schema={{
properties: obj.properties,
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}}
bind:args={
() => value,
(v) => {
value = { ...v, [tagKey]: oneOfSelected }
}
}
{shouldDispatchChanges}
on:change={() => {
dispatch('nestedChange')
}}
on:nestedChange
/>
{/if}
{/key}
{:else if disabled}
<textarea disabled></textarea>
@@ -1107,9 +1177,10 @@
{/if}
</div>
{:else if properties && Object.keys(properties).length > 0 && inputCat !== 'list'}
<div class={hideNested ? 'hidden' : 'py-4 pr-2 pl-6 border rounded-md w-full'}>
<div class={hideNested ? 'hidden' : 'px-3 pt-6 border rounded-md w-full'}>
{#if orderEditable}
<SchemaFormDnd
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
@@ -1140,6 +1211,7 @@
/>
{:else}
<SchemaForm
lightHeaderFont
{nestedClasses}
{onlyMaskPassword}
{disablePortal}
@@ -1190,8 +1262,8 @@
/>
{/await}
{/if}
{#if inputCat == 'list'}
<div class="block">
{#if inputCat == 'list' && !displayHeader}
<div class="block mt-2.5 pl-2">
<Toggle
on:change={(e) => {
isListJson = !isListJson
@@ -1199,7 +1271,7 @@
checked={isListJson}
textClass="text-secondary"
size="xs"
options={{ right: 'json' }}
options={{ left: 'json' }}
/>
</div>
{/if}
@@ -1220,6 +1292,7 @@
dispatch('blur')
}}
enumLabels={extra['enumLabels']}
selectClass="min-h-10"
/>
</div>
{:else if inputCat == 'date'}
@@ -1328,12 +1401,7 @@
use:autosize
onkeydown={onKeyDown}
{disabled}
class={twMerge(
'w-full',
valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-3'
)}
class={twMerge('w-full', inputBaseClass, inputBorderClass({ error: !!error }))}
placeholder={placeholder ?? defaultValue ?? ''}
bind:value
></textarea>
@@ -1344,7 +1412,7 @@
<button
class="absolute {password || extra?.['password'] == true
? 'right-16 top-1.5'
: 'right-1 top-1'} opacity-0 group-hover:opacity-100 duration-200 py-1 min-w-min !px-2 items-center text-gray-800 bg-surface-secondary border rounded center-center hover:bg-gray-300 transition-all cursor-pointer"
: 'right-1 top-[7px]'} opacity-0 group-hover:opacity-100 duration-200 py-1 min-w-min !px-2 items-center text-gray-800 bg-surface-secondary border rounded center-center hover:bg-gray-300 transition-all cursor-pointer"
onclick={() => {
pickForField = label
itemPicker?.openDrawer?.()
@@ -1362,14 +1430,20 @@
{@render actions?.()}
</div>
{#if !compact || (error && error != '')}
<div class="text-right text-xs text-red-600 dark:text-red-400">
{#if disabled || error === ''}
&nbsp;
{:else}
{error}
{/if}
{#if description}
<div class={twMerge('text-2xs italic py-1 text-hint', css?.description?.class)}>
<pre class="font-main whitespace-normal">{description}</pre>
</div>
{/if}
{#if !compact || (error && error != '')}
{#if disabled || error === ''}
&nbsp;
{:else}
<div class="text-right text-xs text-red-600 dark:text-red-400 mb-2">
{error}
</div>
{/if}
{:else if !noMargin}
<div class="mb-2"></div>
{/if}
@@ -1381,10 +1455,4 @@
-webkit-appearance: none !important;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield !important;
appearance: textfield !important;
}
</style>
@@ -10,6 +10,7 @@
import type { SchemaProperty } from '$lib/common'
import Toggle from './Toggle.svelte'
import { tick } from 'svelte'
import Select from './select/Select.svelte'
interface Props {
canEditResourceType?: boolean
@@ -54,39 +55,44 @@
{#if canEditResourceType || originalType == 'string[]' || originalType == 'object[]'}
<Label label="Items type">
<select
bind:value={selected}
onchange={() => {
if (selected == 'enum') {
itemsType = { type: 'string', enum: [] }
} else if (selected == 'string') {
itemsType = { type: 'string' }
} else if (selected == 'number') {
itemsType = { type: 'number' }
} else if (selected == 'object') {
itemsType = { ...itemsType, type: 'object' }
} else if (selected == 'bytes') {
itemsType = { type: 'string', contentEncoding: 'base64' }
} else if (selected == 'resource') {
itemsType = { type: 'resource', resourceType: itemsType?.resourceType }
} else if (selected == 's3object') {
itemsType = { type: 'object', resourceType: 's3object' }
} else {
itemsType = undefined
<Select
bind:value={
() => selected,
(v) => {
selected = v
if (selected == 'enum') {
itemsType = { type: 'string', enum: [] }
} else if (selected == 'string') {
itemsType = { type: 'string' }
} else if (selected == 'number') {
itemsType = { type: 'number' }
} else if (selected == 'object') {
itemsType = { ...itemsType, type: 'object' }
} else if (selected == 'bytes') {
itemsType = { type: 'string', contentEncoding: 'base64' }
} else if (selected == 'resource') {
itemsType = { type: 'resource', resourceType: itemsType?.resourceType }
} else if (selected == 's3object') {
itemsType = { type: 'object', resourceType: 's3object' }
} else {
itemsType = undefined
}
}
}}
id="array-type-narrowing"
>
<option value="string"> Items are strings</option>
<option value="enum">Items are strings from an enum</option>
{#if originalType != 'string[]'}
<option value="s3object">Items are S3 objects</option>
<option value="object"> Items are objects (JSON)</option>
<option value="resource"> Items are resources</option>
<option value="number">Items are numbers</option>
<option value="bytes">Items are bytes</option>
{/if}
</select>
}
items={[
{ value: 'string', label: 'Items are strings' },
{ value: 'enum', label: 'Items are strings from an enum' },
...(originalType != 'string[]'
? [
{ value: 's3object', label: 'Items are S3 objects' },
{ value: 'object', label: 'Items are objects (JSON)' },
{ value: 'resource', label: 'Items are resources' },
{ value: 'number', label: 'Items are numbers' },
{ value: 'bytes', label: 'Items are bytes' }
]
: [])
] as { value: string; label: string }[]}
/>
</Label>
{:else if itemsType?.resourceType}
<Label label="Resource type">
@@ -7,6 +7,7 @@
import { Clock, X } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
import { inputBaseClass, inputBorderClass } from './text_input/TextInput.svelte'
// import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
interface Props {
@@ -110,20 +111,21 @@
bind:value={date}
{autofocus}
{disabled}
class={twMerge('h-8 text-sm !w-3/4 ', inputClass)}
class={twMerge(inputBaseClass, inputBorderClass(), 'text-sm !w-3/4 ', inputClass)}
min={minDate}
max={maxDate}
/>
<input
type="time"
bind:value={time}
class={twMerge('h-8 text-sm !w-1/4 min-w-[100px] ', inputClass)}
class={twMerge(inputBaseClass, inputBorderClass(), 'text-sm !w-1/4 min-w-[100px] ', inputClass)}
{disabled}
/>
<Button
variant="border"
variant="contained"
color="light"
wrapperClasses="h-8"
wrapperClasses="h-full"
btnClasses="bg-surface-secondary"
startIcon={{
icon: Clock
}}
@@ -168,7 +170,7 @@
<Button
variant="border"
color="light"
wrapperClasses="h-8"
wrapperClasses="h-full"
{disabled}
on:click={() => {
value = undefined
@@ -358,11 +358,11 @@
['String', 'string'],
['Number', 'number'],
['Integer', 'integer'],
['Object', 'object'],
['Boolean', 'boolean'],
['Resource', 'resource'],
['Object', 'object'],
['OneOf', 'oneOf'],
['Array', 'array'],
['Boolean', 'boolean'],
['S3', 'S3']
])
@@ -530,7 +530,7 @@
{:else}
<!-- WIP -->
{#if jsonEnabled && customUi?.jsonOnly != true}
<div class="w-full p-3 flex gap-4 justify-end items-center">
<div class="w-full px-3 flex gap-4 justify-end items-center">
{#if addPropertyInEditorTab}
<AddPropertyV2
bind:schema
@@ -549,8 +549,8 @@
label="JSON View"
size="xs"
options={{
right: 'JSON editor',
rightTooltip:
left: 'JSON editor',
leftTooltip:
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
}}
lightMode
@@ -8,6 +8,7 @@
autoheight?: boolean
lineNumbersWidth?: number
lineNumbersOffset?: number
showNumbers?: boolean
class?: string
fontSize?: number
}
@@ -18,6 +19,7 @@
lineNumbersWidth = 51,
lineNumbersOffset = 0,
class: className = '',
showNumbers = true,
fontSize = 14
}: Props = $props()
@@ -76,7 +78,9 @@
class="margin-view-overlays"
role="presentation"
aria-hidden="true"
style="position: absolute; font-family: {fontFamily}; font-weight: normal; font-size: {fontSize}px; font-feature-settings: 'liga' 0, 'calt' 0; font-variation-settings: normal; line-height: {lineHeight}px; letter-spacing: 0px; width: {lineNumbersWidth}px; height: 4893px;"
style="position: absolute; font-family: {fontFamily}; font-weight: normal; font-size: {fontSize}px; font-feature-settings: 'liga' 0, 'calt' 0; font-variation-settings: normal; line-height: {lineHeight}px; letter-spacing: 0px; width: {lineNumbersWidth}px; height: 4893px; display: {!showNumbers
? 'none'
: 'block'};"
>
{#each lines as _, i}
<div style="top:{lineHeight * i}px;height:{lineHeight}px;">
@@ -40,17 +40,15 @@
{/if}
{#if displayType}
{#if format && !format.startsWith('resource') && !format.startsWith('jsonschema-')}
<span class="text-xs italic ml-2 text-tertiary dark:text-indigo-400">
<span class="text-xs italic ml-2 text-hint">
{#if format && !format.startsWith('resource') && !format.startsWith('jsonschema-')}
{format}
</span>
{:else}
<span class="text-xs italic ml-2 text-tertiary dark:text-indigo-400">
{:else}
{type ?? 'any'}{contentEncoding && contentEncoding != ''
? `, encoding: ${contentEncoding}`
: ''}
</span>
{/if}
{/if}
</span>
{/if}
{#if !emptyString(simpleTooltip)}
@@ -2,9 +2,11 @@
import { Plug } from 'lucide-svelte'
import { Button } from './common'
import AnimatedButton from './common/button/AnimatedButton.svelte'
import { twMerge } from 'tailwind-merge'
export let connecting: boolean
export let id: undefined | string = undefined
export let wrapperClasses = ''
</script>
<AnimatedButton animate={connecting} baseRadius="6px" animationDuration="2s" marginWidth="2px">
@@ -12,10 +14,14 @@
variant="border"
color="light"
size="xs2"
btnClasses={connecting ? 'text-blue-500' : 'text-primary'}
btnClasses={twMerge(
connecting ? 'text-blue-500' : 'text-tertiary',
'group/plug-btn overflow-clip flex p-0'
)}
on:click
{...id ? { id } : {}}
{wrapperClasses}
>
<Plug size={16} /> &rightarrow;
<Plug size={14} />
</Button>
</AnimatedButton>
@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import {
type Folder,
@@ -19,7 +17,7 @@
import { Eye, Plus } from 'lucide-svelte'
import Label from './Label.svelte'
import { sendUserToast } from '$lib/toast'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import Select from './select/Select.svelte'
import { safeSelectItems } from './select/utils.svelte'
@@ -139,9 +137,11 @@
dispatch('update')
loadFolder()
}
run(() => {
$effect.pre(() => {
if ($workspaceStore && $userStore) {
load()
untrack(() => {
load()
})
}
})
</script>
@@ -327,7 +327,6 @@
/>
<ToggleButton
position="center"
value="writer"
label="Writer"
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
@@ -335,7 +334,6 @@
/>
<ToggleButton
position="right"
value="admin"
label="Admin"
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
+152 -142
View File
@@ -7,7 +7,7 @@
type InstanceGroup
} from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { Button } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
import TableCustom from './TableCustom.svelte'
@@ -20,16 +20,20 @@
import Select from './select/Select.svelte'
import { safeSelectItems } from './select/utils.svelte'
export let name: string
let can_write = false
interface Props {
name: string
}
let { name }: Props = $props()
let can_write = $state(false)
type Role = 'member' | 'manager' | 'admin'
let group: Group | undefined
let instance_group: InstanceGroup | undefined
let members: { member_name: string; role: Role }[] | undefined = undefined
let usernames: string[] | undefined = []
let username: string = ''
let summary = ''
let instance_group: InstanceGroup | undefined = $state()
let members: { member_name: string; role: Role }[] | undefined = $state(undefined)
let usernames: string[] | undefined = $state([])
let username: string = $state('')
let summary = $state('')
const dispatch = createEventDispatcher()
@@ -37,12 +41,6 @@
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
}
$: {
if ($workspaceStore && $userStore) {
load()
}
}
async function load() {
return Promise.all([loadGroup(), loadInstanceGroup(), loadUsernames()])
}
@@ -104,6 +102,13 @@
return 'member'
}
}
$effect.pre(() => {
if ($workspaceStore && $userStore) {
untrack(() => {
load()
})
}
})
</script>
<Section label="Metadata" class="mb-4">
@@ -138,150 +143,155 @@
{/if}
{#if members}
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user</th>
<th></th>
<th></th>
</tr>
<tbody slot="body">
{#each members as { member_name, role }}<tr>
<td>{member_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInGroup = (group?.members ?? []).includes(group)
// const inAcl = (
// group?.extra_perms ? Object.keys(group?.extra_perms) : []
// ).includes(group)
if (role == 'member') {
await GroupService.addUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: {
username: member_name
}
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name
}
})
} else if (role == 'manager') {
await GroupService.removeUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: {
username: member_name
}
})
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name,
write: true
}
})
} else if (role == 'admin') {
await GroupService.addUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: {
username: member_name
}
})
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name,
write: true
}
})
}
loadGroup()
}}
>
{#snippet children({ item })}
<ToggleButton
value="member"
size="xs"
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
{item}
/>
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
{item}
/>
{#if role === 'manager'}
{#snippet body()}
<tbody>
{#each members ?? [] as { member_name, role }}<tr>
<td>{member_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInGroup = (group?.members ?? []).includes(group)
// const inAcl = (
// group?.extra_perms ? Object.keys(group?.extra_perms) : []
// ).includes(group)
if (role == 'member') {
await GroupService.addUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: {
username: member_name
}
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name
}
})
} else if (role == 'manager') {
await GroupService.removeUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: {
username: member_name
}
})
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name,
write: true
}
})
} else if (role == 'admin') {
await GroupService.addUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: {
username: member_name
}
})
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name,
write: true
}
})
}
loadGroup()
}}
>
{#snippet children({ item })}
<ToggleButton
value="manager"
size="xs"
label="Manager"
tooltip="A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member"
value="member"
small
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
{item}
/>
{/if}
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write}
<button
class="ml-2 text-red-500"
on:click={async () => {
await GroupService.removeUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: { username: member_name }
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name
}
})
loadGroup()
}}>remove</button
>
{/if}</td
>
</tr>{/each}
</tbody>
<ToggleButton
value="admin"
small
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
{item}
/>
{#if role === 'manager'}
<ToggleButton
value="manager"
small
label="Manager"
tooltip="A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member"
{item}
/>
{/if}
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write}
<button
class="ml-2 text-red-500"
onclick={async () => {
await GroupService.removeUserToGroup({
workspace: $workspaceStore ?? '',
name,
requestBody: { username: member_name }
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name
}
})
loadGroup()
}}>remove</button
>
{/if}</td
>
</tr>{/each}
</tbody>
{/snippet}
</TableCustom>
{#if instance_group?.emails}
<h2 class="mt-10">Members from the instance group</h2>
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user</th>
</tr>
<tbody slot="body">
{#each instance_group?.emails ?? [] as email}<tr>
<td>{email}</td>
</tr>{/each}
</tbody>
{#snippet body()}
<tbody>
{#each instance_group?.emails ?? [] as email}<tr>
<td>{email}</td>
</tr>{/each}
</tbody>
{/snippet}
</TableCustom>
{/if}
{:else}
@@ -23,7 +23,6 @@
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { tick } from 'svelte'
import { fade } from 'svelte/transition'
import { buildPrefixRegex } from './flows/previousResults'
import type VariableEditor from './VariableEditor.svelte'
import type ItemPicker from './ItemPicker.svelte'
@@ -39,6 +38,7 @@
import FlowPlugConnect from './FlowPlugConnect.svelte'
import { deepEqual } from 'fast-equals'
import S3ArrayHelperButton from './S3ArrayHelperButton.svelte'
import { inputBorderClass } from './text_input/TextInput.svelte'
interface Props {
schema: Schema | { properties?: Record<string, any>; required?: string[] }
@@ -470,18 +470,10 @@
)
</script>
{#if arg != undefined && !hidden}
<div
class={twMerge(
'pl-2 pt-2 pb-2 ml-2 relative hover:bg-surface hover:shadow-md transition-all duration-200',
$propPickerConfig?.propName == argName
? 'bg-surface border-l-4 border-blue-500 shadow-md rounded-l-md z-50 '
: 'hover:rounded-md',
className
)}
>
<div class="flex flex-row justify-between gap-1 pb-1 px-2">
<div class="flex flex-wrap grow">
{#if arg != undefined}
<div class={twMerge('pt-2 pb-2 relative group', className)}>
<div class="flex flex-row justify-between gap-1 pb-1">
<div class="flex flex-wrap grow min-h-7 items-end">
<FieldHeader
label={argName}
simpleTooltip={headerTooltip}
@@ -496,7 +488,7 @@
{#if isStaticTemplate(inputCat)}
<div>
<span
class="bg-blue-100 text-blue-800 text-sm font-medium mr-2 px-2.5 !py-0.5 rounded ml-2 {propertyType ==
class="border text-gray-400 dark:text-gray-500 text-2xs font-medium mr-2 px-1 !py-[1px] rounded ml-2.5 {propertyType ==
'static' && arg.type === 'javascript'
? 'visible'
: 'invisible'}"
@@ -507,7 +499,9 @@
{/if}
</div>
{#if !noDynamicToggle}
<div class="flex flex-row gap-x-2 gap-y-1 flex-wrap z-10 items-center">
<div
class="flex flex-row gap-x-2 z-10 absolute right-0 group-hover:bg-surface transition-colors"
>
{#if enableAi}
<StepInputGen
bind:this={stepInputGen}
@@ -536,8 +530,40 @@
}}
{pickableProperties}
{argName}
btnClass={twMerge(
'h-6 min-w-8 px-2',
'group-hover:opacity-100 transition-opacity',
!connecting ? 'opacity-0' : ''
)}
/>
{/if}
{#if propPickerWrapperContext}
<FlowPlugConnect
wrapperClasses={twMerge(
connecting ? 'h-5 w-7' : 'h-6 w-8',
'group-hover:opacity-100 transition-opacity p-0',
!connecting ? 'opacity-0' : ''
)}
id="flow-editor-plug"
{connecting}
on:click={() => {
if (
$propPickerConfig?.propName == argName &&
$propPickerConfig?.insertionMode == 'connect'
) {
clearFocus()
} else {
focusProp?.(argName, 'connect', (path) => {
connectProperty(path)
dispatch('change', { argName })
return true
})
}
}}
/>
{/if}
<div>
<ToggleButtonGroup
selected={propertyType}
@@ -593,19 +619,18 @@
propertyType = 'static'
}
}}
class="h-6"
>
{#snippet children({ item })}
{#if isStaticTemplate(inputCat)}
<ToggleButton
tooltip={`Write text or surround javascript with \`\$\{\` and \`\}\`. Use \`results\` to connect to another node\'s output.`}
light
value="static"
size="xs2"
label={'${}'}
{item}
/>
{:else}
<ToggleButton small label="Static" value="static" {item} />
<ToggleButton small label="static" value="static" {item} />
{/if}
{#if codeInjectionDetected && propertyType == 'static'}
@@ -634,27 +659,6 @@
{/snippet}
</ToggleButtonGroup>
</div>
{#if propPickerWrapperContext && inputCat !== 'dynamic'}
<FlowPlugConnect
id="flow-editor-plug"
{connecting}
on:click={() => {
if (
$propPickerConfig?.propName == argName &&
$propPickerConfig?.insertionMode == 'connect'
) {
clearFocus()
} else {
focusProp?.(argName, 'connect', (path) => {
connectProperty(path)
dispatch('change', { argName })
return true
})
}
}}
/>
{/if}
</div>
{/if}
</div>
@@ -674,12 +678,7 @@
<div class="relative flex flex-row items-top gap-2 justify-between">
<div class="min-w-0 grow">
{#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle}
{#if argName && schema?.properties?.[argName]?.description}
<div class="text-xs italic pb-1 text-secondary">
<pre class="font-main">{schema.properties[argName].description}</pre>
</div>
{/if}
<div class="mt-2 min-h-[28px]">
<div>
{#if arg}
<TemplateEditor
bind:this={monacoTemplate}
@@ -694,8 +693,14 @@
dispatch('change', { argName, arg })
}}
loadAsync
class="bg-surface-secondary"
/>
{/if}
{#if argName && schema?.properties?.[argName]?.description}
<div class="text-xs italic py-1 text-hint">
<pre class="font-main">{schema.properties[argName].description}</pre>
</div>
{/if}
</div>
{:else if (propertyType === undefined || propertyType == 'static') && schema?.properties?.[argName]}
<ArgInput
@@ -743,23 +748,31 @@
transform?.type === 'static' ? transform.value : transform?.expr
])
)}
/>
{#if shouldShowS3ArrayHelper}
<S3ArrayHelperButton
{connecting}
onClick={() =>
switchToJsAndConnect((path) => appendPathToArrayExpr(arg.expr, path))}
/>
{/if}
>
{#snippet innerBottomSnippet()}
{#if shouldShowS3ArrayHelper}
<S3ArrayHelperButton
{connecting}
onClick={() =>
switchToJsAndConnect((path) => appendPathToArrayExpr(arg.expr, path))}
/>
{/if}
{/snippet}
</ArgInput>
{:else if arg.expr != undefined}
<div class="border mt-2">
<div
class={`bg-surface-secondary rounded-md flex flex-col pl-4 ${inputBorderClass({ forceFocus: focused })}`}
>
<SimpleEditor
bind:this={monaco}
bind:code={arg.expr}
yPadding={8}
{extraLib}
lang="javascript"
shouldBindKey={false}
renderLineHighlight="none"
hideLineNumbers
fakeMonacoPlaceholderClass="mt-2"
on:focus={() => {
focused = true
focusProp?.(argName, 'insert', (path) => {
@@ -778,12 +791,10 @@
/>
<!-- <input type="text" bind:value={arg.expr} /> -->
</div>
{#if !hideHelpButton}
<DynamicInputHelpBox />
{/if}
{#if shouldShowS3ArrayHelper}
<S3ArrayHelperButton
class="mt-2"
{connecting}
onClick={() =>
focusProp?.(argName, 'connect', (path) => {
@@ -793,9 +804,15 @@
/>
{/if}
{#if !hideHelpButton}
<DynamicInputHelpBox />
{/if}
<div class="mb-2"></div>
{:else}
Not recognized input type {argName} ({arg.expr}, {propertyType})
<span class="text-xs text-red-500">
Not recognized input type {argName} ({arg.expr}, {propertyType})
</span>
<div class="flex mt-2">
<Button
variant="border"
@@ -807,29 +824,6 @@
>
{/if}
</div>
{#if $propPickerConfig?.propName == argName}
<div
class="text-blue-500 absolute top-2 lg:-right-2.5 -right-1"
in:fade={{ duration: 200 }}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="24 24 12 12 24 0" />
</svg>
</div>
{:else}
<div class="w-0"></div>
{/if}
</div>
</div>
</div>
@@ -11,6 +11,7 @@
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
import { Plus } from 'lucide-svelte'
import ResizeTransitionWrapper from './common/ResizeTransitionWrapper.svelte'
interface Props {
schema: Schema | { properties?: Record<string, any> }
@@ -80,9 +81,9 @@
})
</script>
<div class="w-full {clazz}">
<div class="w-full mb-6 {clazz}">
{#if enableAi}
<div class="px-0.5 pt-0.5">
<div class="mt-2">
<StepInputsGen
{pickableProperties}
argNames={keys
@@ -101,7 +102,7 @@
{#if keys.length > 0}
{#each keys as argName, index (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
<div class="pt-2 relative">
<ResizeTransitionWrapper class="mt-2 relative" innerClass="w-full" vertical>
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
@@ -122,7 +123,7 @@
Object.entries(args ?? {}).filter(([key]) => key !== argName)
)}
/>
</div>
</ResizeTransitionWrapper>
{/if}
{/each}
{:else}
+14 -4
View File
@@ -7,6 +7,8 @@
import { createEventDispatcher } from 'svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
import Button from './common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import { inputBorderClass } from './text_input/TextInput.svelte'
interface Props {
code: string | undefined
@@ -34,6 +36,7 @@
let tooBig = $derived(code && code?.length > 1000000)
let loadTooBigAnyway = $state(false)
let focused = $state(false)
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
@@ -66,12 +69,17 @@
</div>
{:else}
<div class="flex flex-col w-full">
<div class="border w-full">
<div
class={twMerge(
'w-full rounded-md bg-surface-secondary',
inputBorderClass({ error: !!error, forceFocus: focused })
)}
>
<SimpleEditor
{loadAsync}
{small}
on:focus
on:blur
on:focus={() => (dispatch('focus'), (focused = true))}
on:blur={() => (dispatch('blur'), (focused = false))}
bind:this={editor}
on:change
autoHeight
@@ -80,10 +88,12 @@
class={clazz}
{disabled}
{fixedOverflowWidgets}
renderLineHighlight="none"
yPadding={8}
/>
</div>
{#if error != ''}
<span class="text-red-600 text-xs">{error}</span>
<span class="text-red-600 text-xs mt-1">{error}</span>
{/if}
</div>
{/if}
@@ -49,7 +49,7 @@
<!-- Add a hidden button that can receive focus -->
<button bind:this={focusTrap} class="sr-only" tabindex="-1" aria-hidden="true">Focus trap</button>
<div class="h-full">
<div class="h-full py-3 rounded-md border">
<SimpleEditor
bind:this={simpleEditor}
on:focus={() => {
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import ResizeTransitionWrapper from './common/ResizeTransitionWrapper.svelte'
import { allTrue } from '$lib/utils'
import { RefreshCw } from 'lucide-svelte'
import ArgInput from './ArgInput.svelte'
@@ -115,12 +115,14 @@
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if Object.keys(schema.properties ?? {}).includes(argName)}
<div
<ResizeTransitionWrapper
vertical
class={twMerge(
'flex gap-2',
'flex gap-2 relative',
animateArg === argName && 'animate-pulse ring-2 ring-offset-2 ring-blue-500 rounded'
)}
data-arg={argName}
innerClass="w-full"
outerDivProps={{ 'data-arg': argName }}
>
{#if schema?.properties?.[argName]}
<ArgInput
@@ -150,22 +152,23 @@
nullable={schema.properties[argName].nullable}
title={schema.properties[argName].title}
placeholder={schema.properties[argName].placeholder}
/>
>
{#snippet fieldHeaderActions()}
{#if stepsInputArgs?.isArgManuallySet(mod.id, argName)}
<Button
on:click={() => {
plugIt(argName)
}}
size="xs2"
variant="contained"
color="light"
title="Re-evaluate input step"><RefreshCw size={12} /></Button
>
{/if}
{/snippet}
</ArgInput>
{/if}
{#if stepsInputArgs?.isArgManuallySet(mod.id, argName)}
<div class="pt-6 mt-0.5">
<Button
on:click={() => {
plugIt(argName)
}}
size="sm"
variant="border"
color="light"
title="Re-evaluate input step"><RefreshCw size={14} /></Button
>
</div>
{/if}
</div>
</ResizeTransitionWrapper>
{/if}
{/each}
{/if}
@@ -3,6 +3,8 @@
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
import { selectOptions } from './apps/editor/component'
import Select from './select/Select.svelte'
import TextInput from './text_input/TextInput.svelte'
interface Props {
min: number | undefined
@@ -44,7 +46,11 @@
options={{ right: 'Enabled' }}
size="xs"
/>
{/snippet} <input type="number" bind:value={min} disabled={!minChecked} />
{/snippet}
<TextInput
inputProps={{ type: 'number', disabled: !minChecked }}
bind:value={() => min?.toString(), (v) => (min = v ? parseInt(v) : undefined)}
/>
</Label>
<Label label="Max" class="w-full col-span-1 ">
@@ -68,31 +74,41 @@
size="xs"
/>
{/snippet}
<input type="number" bind:value={max} disabled={!maxChecked} />
<TextInput
inputProps={{ type: 'number', disabled: !maxChecked }}
bind:value={() => max?.toString(), (v) => (max = v ? parseInt(v) : undefined)}
/>
</Label>
</div>
<div class="grid grid-cols-3 gap-4">
<Label label="Currency" class="w-full col-span-2">
<div class="flex gap-2">
<Label label="Currency" class=" col-span-2">
{#snippet header()}
<Tooltip light small>
Select a currency to display the number in. If a currency is selected, you can also select
a locale to format the number according to that locale.
</Tooltip>
{/snippet}
<select bind:value={currency}>
<option value={undefined}> No currency </option>
{#each selectOptions.currencyOptions as c}
<option value={c}>{c}</option>
{/each}
</select>
<Select
bind:value={
() => currency,
(v) => {
currency = v
if (!v) currencyLocale = undefined
}
}
items={selectOptions.currencyOptions.map((c) => ({ label: c, value: c }))}
placeholder="No currency"
clearable
/>
</Label>
<Label label="Currency locale" class="w-full">
<select bind:value={currencyLocale} disabled={!currency}>
<option value={undefined}> No locale </option>
{#each selectOptions.localeOptions as c}
<option value={c}>{c}</option>
{/each}
</select>
<Select
bind:value={currencyLocale}
items={selectOptions.localeOptions.map((c) => ({ label: c, value: c }))}
placeholder="No locale"
disabled={!currency}
clearable
/>
</Label>
</div>
</div>
+2 -8
View File
@@ -420,10 +420,7 @@
<ToggleButton
icon={User}
disabled={disabled || disableEditing}
light
size="xs"
value="user"
position="left"
label="User"
{item}
/>
@@ -431,10 +428,7 @@
<ToggleButton
icon={Folder}
disabled={disabled || disableEditing}
light
size="xs"
value="folder"
position="right"
label="Folder"
{item}
/>
@@ -443,7 +437,7 @@
</div>
{/if}
{#if !hideUser}
<div class="text-xl">/</div>
<div class="text-xl text-tertiary">/</div>
{/if}
<div>
{#if meta.ownerKind === 'user'}
@@ -465,7 +459,7 @@
</label>
{/if}
</div>
<span class="text-xl">/</span>
<span class="text-xl text-tertiary">/</span>
<label class="block grow w-full max-w-md">
<!-- svelte-ignore a11y_autofocus -->
<input
@@ -6,11 +6,13 @@
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
import { Button } from './common'
import { Pen, Plus, RotateCw } from 'lucide-svelte'
import { Loader2, Pen, Plus, RotateCw } from 'lucide-svelte'
import { sendUserToast } from '$lib/toast'
import Select from './select/Select.svelte'
import DbManagerDrawer from './DBManagerDrawer.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
import { twMerge } from 'tailwind-merge'
import DropdownV2 from './DropdownV2.svelte'
interface Props {
initialValue?: string | undefined
@@ -24,6 +26,7 @@
expressOAuthSetup?: boolean
defaultValues?: Record<string, any> | undefined
placeholder?: string | undefined
selectInputClass?: string
onClear?: () => void
excludedValues?: string[]
}
@@ -40,6 +43,7 @@
expressOAuthSetup = false,
defaultValues = undefined,
placeholder = undefined,
selectInputClass = '',
onClear = undefined,
excludedValues = undefined
}: Props = $props()
@@ -176,91 +180,102 @@
}}
/>
<!-- {JSON.stringify({ value, collection })} -->
<div class="flex flex-col w-full items-start min-h-9">
<div class="flex flex-col w-full items-start min-h-10">
<div class="flex flex-row w-full items-center">
{#if collection?.length > 0}
<Select
{disabled}
{disablePortal}
bind:value={
() => value,
(v) => {
value = v
valueType = collection.find((x) => x?.value == v)?.type
}
<Select
{disabled}
{disablePortal}
bind:value={
() => value,
(v) => {
value = v
valueType = collection.find((x) => x?.value == v)?.type
}
onClear={() => {
initialValue = undefined
value = undefined
valueType = undefined
onClear?.()
}}
items={collection}
clearable
class="text-clip grow min-w-0"
placeholder={placeholder ?? `${resourceType ?? 'any'} resource`}
/>
{:else if !loading}
<div class="text-2xs text-tertiary mr-2">0 found</div>
{/if}
{#if !loading}
<div class="mx-0.5"></div>
{#if value && value != ''}
}
onClear={() => {
initialValue = undefined
value = undefined
valueType = undefined
onClear?.()
}}
items={collection}
clearable
class="text-clip grow min-w-0"
inputClass={twMerge('min-h-10', selectInputClass)}
placeholder={placeholder ?? `${resourceType ?? 'any'} resource`}
itemLabelWrapperClasses="flex-1"
itemButtonWrapperClasses="flex"
>
{#snippet endSnippet({ item, close })}
<Button
{disabled}
color="light"
variant="contained"
size="sm"
btnClasses="w-8 px-0.5 py-1.5"
on:click={() => resourceEditor?.initEdit?.(value ?? '')}
size="xs3"
btnClasses="w-8 px-0.5 py-1.5 bg-transparent hover:bg-surface-secondary"
wrapperClasses="-mr-2 pl-1"
on:click={() => (resourceEditor?.initEdit?.(item.value ?? ''), close())}
startIcon={{ icon: Pen }}
iconOnly
/>
{/if}
{#if resourceType?.includes(',')}
{#each resourceType.split(',') as rt}
{/snippet}
{#snippet bottomSnippet({ close })}
<div class="flex bg-surface border-t divide-x">
{#if resourceType?.includes(',')}
<DropdownV2
enableFlyTransition
items={resourceType?.split(',').map((rt) => ({
displayName: `${rt} resource`,
icon: Plus,
action: () => (appConnect?.open?.(rt), close())
})) ?? []}
>
{#snippet buttonReplacement()}
<Button
{disabled}
color="light"
variant="contained"
wrapperClasses="flex-1"
btnClasses="rounded-none mt-0.5"
size="sm"
startIcon={{ icon: Plus }}
>
Add a resource
</Button>
{/snippet}
</DropdownV2>
{:else}
<Button
{disabled}
color="light"
variant="contained"
wrapperClasses="flex-1"
btnClasses="rounded-none"
size="sm"
on:click={() => (appConnect?.open?.(resourceType), close())}
startIcon={{ icon: Plus }}
>
Add a {resourceType} resource
</Button>
{/if}
<Button
{disabled}
color="light"
variant="contained"
color="light"
btnClasses="rounded-none"
size="sm"
btnClasses="w-8 px-0.5 py-1.5"
on:click={() => appConnect?.open?.(rt)}
startIcon={{ icon: Plus }}>{rt}</Button
>
{/each}
{:else}
<Button
{disabled}
color="light"
variant="border"
size="sm"
on:click={() => appConnect?.open?.(resourceType)}
startIcon={{ icon: Plus }}
iconOnly={collection?.length > 0}
>{#if collection?.length == 0}
Add a {resourceType} resource
{/if}</Button
>
<div class="mx-0.5"></div>
{/if}
{/if}
<Button
variant="contained"
color="light"
btnClasses="w-8 px-0.5 py-1.5"
size="sm"
on:click={() => {
loadResources(resourceType)
}}
startIcon={{ icon: RotateCw }}
iconOnly
/>
on:click={() => {
loadResources(resourceType)
}}
startIcon={loading ? { icon: Loader2, classes: 'animate-spin' } : { icon: RotateCw }}
iconOnly
/>
</div>
{/snippet}
</Select>
</div>
{#if showSchemaExplorer && value && assetCanBeExplored({ kind: 'resource', path: value }, { resource_type: resourceType })}
<ExploreAssetButton
class="mt-1"
_resourceMetadata={{ resource_type: resourceType }}
asset={{ kind: 'resource', path: value }}
{dbManagerDrawer}
@@ -6,15 +6,27 @@
label?: string
onClick: () => void
connecting?: boolean
class?: string
}
let { label = 'Add object from an expression', onClick, connecting = false }: Props = $props()
let {
label = 'Add object from an expression',
onClick,
connecting = false,
class: className = ''
}: Props = $props()
</script>
{#if !connecting}
<div class="mt-2 mb-2">
<Button variant="border" color="light" size="xs" startIcon={{ icon: Plug }} onclick={onClick}>
{label}
</Button>
</div>
<Button
variant="border"
color="light"
size="xs"
btnClasses="font-normal text-tertiary"
startIcon={{ icon: Plug }}
onclick={onClick}
wrapperClasses={className}
>
{label}
</Button>
{/if}
@@ -819,7 +819,7 @@
<div class="flex gap-1">
{#if secondaryStorageNames.value?.length}
<Select
inputClass="h-10 min-w-44 !placeholder-primary"
inputClass="h-10 min-w-44 !placeholder-secondary"
items={[
{ value: undefined, label: 'Default storage' },
...secondaryStorageNames.value.map((value) => ({ value }))
+18 -8
View File
@@ -19,6 +19,8 @@
import { dragHandleZone, type Options as DndOptions } from '@windmill-labs/svelte-dnd-action'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { ComponentCustomCSS } from './apps/types'
import ResizeTransitionWrapper from './common/ResizeTransitionWrapper.svelte'
import { twMerge } from 'tailwind-merge'
interface Props {
schema: Schema | any
@@ -56,6 +58,7 @@
displayType?: boolean
appPath?: string | undefined
className?: string
lightHeaderFont?: boolean
computeS3ForceViewerPolicies?:
| (() =>
| {
@@ -106,6 +109,7 @@
displayType = true,
appPath = undefined,
className = '',
lightHeaderFont = false,
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
actions: actions_render = undefined
@@ -269,10 +273,15 @@
{#if keys.length > 0 && args}
{#each fields as item, i (item.id)}
{@const argName = item.value}
<div
class={typeof diff[argName] === 'object' && diff[argName].diff !== 'same'
? 'bg-red-300 dark:bg-red-800 rounded-md'
: ''}
<ResizeTransitionWrapper
vertical
class={twMerge(
typeof diff[argName] === 'object' &&
diff[argName].diff !== 'same' &&
'bg-red-300 dark:bg-red-800 rounded-md',
'relative'
)}
innerClass="w-full"
>
<!-- svelte-ignore a11y_click_events_have_key_events -->
{#if !hiddenArgs.includes(argName) && keys.includes(argName)}
@@ -280,6 +289,7 @@
{@const formerProperty = diff[argName].oldSchema}
<div class="px-2">
<ArgInput
{lightHeaderFont}
{disablePortal}
{resourceTypes}
{prettifyHeader}
@@ -342,6 +352,7 @@
{JSON.stringify(args?.[argName])} -->
{#if !hidden[argName]}
<ArgInput
{lightHeaderFont}
on:change={() => {
dispatch('change')
}}
@@ -419,15 +430,14 @@
{#snippet children({ item })}
<ToggleButton
value="inlined"
size="sm"
small
label="Inlined"
tooltip="The value is inlined in the resource and thus has no special treatment."
{item}
/>
<ToggleButton
position="right"
value="secret"
size="sm"
small
label="Secret"
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
{item}
@@ -443,7 +453,7 @@
{/if}
</div>
{/if}
</div>
</ResizeTransitionWrapper>
{/each}
{:else if !shouldHideNoInputs}
<div class="text-secondary text-sm">No inputs</div>
@@ -537,7 +537,7 @@
<SplitPanesWrapper>
<Splitpanes class="!overflow-visible">
<Pane bind:size={codePanelSize} minSize={10} class="!overflow-visible">
<div class="h-full !overflow-visible bg-gray-50 dark:bg-[#272D38] relative">
<div class="h-full !overflow-visible bg-surface dark:bg-[#272D38] relative">
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
{#if assets?.length}
<AssetsDropdownButton {assets} />
@@ -129,8 +129,8 @@
<div>
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (owner = '')}>
{#snippet children({ item })}
<ToggleButton value="user" size="xs" label="User" {item} />
<ToggleButton value="group" size="xs" label="Group" {item} />
<ToggleButton value="user" small label="User" {item} />
<ToggleButton value="group" small label="Group" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -175,8 +175,8 @@
}}
>
{#snippet children({ item })}
<ToggleButton value="viewer" size="xs" label="Viewer" {item} />
<ToggleButton value="writer" size="xs" label="Writer" {item} />
<ToggleButton value="viewer" small label="Viewer" {item} />
<ToggleButton value="writer" small label="Writer" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -85,10 +85,13 @@
allowVim = false,
tailwindClasses = [],
class: className = '',
fakeMonacoPlaceholderClass = '',
loadAsync = false,
key,
disabled = false,
minHeight = 1000
minHeight = 1000,
renderLineHighlight = 'none',
yPadding
}: {
lang: string
code?: string
@@ -110,11 +113,14 @@
allowVim?: boolean
tailwindClasses?: string[]
class?: string
fakeMonacoPlaceholderClass?: string
loadAsync?: boolean
initialCursorPos?: IPosition
key?: string
disabled?: boolean
minHeight?: number
renderLineHighlight?: 'all' | 'line' | 'gutter' | 'none'
yPadding?: number
} = $props()
const dispatch = createEventDispatcher()
@@ -332,6 +338,8 @@
$relativeLineNumbers
),
model,
...(yPadding !== undefined ? { padding: { bottom: yPadding, top: yPadding } } : {}),
renderLineHighlight,
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
fontSize: fontSize,
@@ -602,9 +610,11 @@
<FakeMonacoPlaceHolder
{code}
autoheight
lineNumbersWidth={(23 * fontSize) / 14}
lineNumbersWidth={hideLineNumbers ? 0 : (23 * fontSize) / 14}
lineNumbersOffset={fontSize == 14 ? -8 : -11}
{fontSize}
showNumbers={!hideLineNumbers}
class={fakeMonacoPlaceholderClass}
/>
{/if}
@@ -11,6 +11,7 @@
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import RegexGen from './copilot/RegexGen.svelte'
import TextInput from './text_input/TextInput.svelte'
interface Props {
pattern: string | undefined
@@ -341,7 +342,10 @@
{:else if kind == 'none'}
{#if !noExtra}
<Label label="Min textarea rows">
<input type="number" bind:value={minRows} />
<TextInput
inputProps={{ type: 'number' }}
bind:value={() => minRows?.toString(), (v) => (minRows = v ? parseInt(v) : undefined)}
/>
</Label>
{/if}
{:else if kind === 'base64'}
@@ -300,17 +300,17 @@
}}
>
{#snippet children({ item })}
<ToggleButton value={'user'} size="xs" label="User" {item} />
<ToggleButton value={'user'} small label="User" {item} />
<ToggleButton
value={'devops'}
size="xs"
small
label="Devops"
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
{item}
/>
<ToggleButton
value={'super_admin'}
size="xs"
small
label="Superadmin"
{item}
/>
@@ -26,6 +26,7 @@
import EditorTheme from './EditorTheme.svelte'
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
import { setMonacoJsonOptions } from './monacoLanguagesOptions'
import { inputBorderClass } from './text_input/TextInput.svelte'
export const conf = {
wordPattern:
@@ -449,7 +450,14 @@
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
fontSize,
suggestOnTriggerCharacters: true
suggestOnTriggerCharacters: true,
renderLineHighlight: 'none',
lineNumbers: 'off',
padding: {
bottom: 8,
top: 8
}
})
} catch (e) {
console.error('Error loading monaco:', e)
@@ -498,10 +506,12 @@
editor.onDidFocusEditorText(() => {
dispatch('focus')
isFocus = true
})
editor.onDidBlurEditorText(() => {
dispatch('blur')
isFocus = false
updateCode()
})
@@ -597,6 +607,7 @@
editor?.focus()
}
let isFocus = false
let mounted = false
let loadTimeout: number | undefined = undefined
onMount(async () => {
@@ -650,19 +661,18 @@
{#if !editor}
<FakeMonacoPlaceHolder
autoheight
showNumbers={false}
{code}
lineNumbersWidth={23}
lineNumbersOffset={-8}
class="border template nonmain-editor rounded min-h-4 mx-0.5 overflow-clip"
lineNumbersWidth={14}
lineNumbersOffset={-20}
class="template nonmain-editor rounded-md min-h-4 bg-surface-secondary !py-[9px] overflow-clip"
/>
{/if}
<div
bind:this={divEl}
style="height: 18px;"
class="{$$props.class ??
''} border template nonmain-editor rounded min-h-4 mx-0.5 overflow-clip {!editor
? 'hidden'
: ''}"
style="height: 18px; padding-left: 6px;"
class="{inputBorderClass({ forceFocus: isFocus })} {$$props.class ??
''} template nonmain-editor rounded-md min-h-4 overflow-clip {!editor ? 'hidden' : ''}"
bind:clientWidth={width}
></div>
+17 -7
View File
@@ -5,6 +5,7 @@
import Tooltip from './Tooltip.svelte'
import { AlertTriangle } from 'lucide-svelte'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import { inputBorderClass } from './text_input/TextInput.svelte'
export let options: {
left?: string
@@ -18,7 +19,7 @@
export let disabled = false
export let textClass = ''
export let textStyle = ''
export let color: 'blue' | 'red' | 'nord' = 'blue'
export let color: 'blue' | 'red' | 'nord' = 'nord'
export let id = (Math.random() + 1).toString(36).substring(10)
export let lightMode: boolean = false
export let eeOnly: boolean = false
@@ -43,8 +44,12 @@
{#if Boolean(options?.left)}
<span
class={twMerge(
'mr-2 font-medium duration-50 select-none',
bothOptions || textDisabled ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
'mr-2 font-normal duration-50 select-none',
bothOptions || textDisabled
? checked
? 'text-disabled'
: 'text-tertiary'
: 'text-tertiary',
size === 'xs' || size === '2sm' ? 'text-xs' : size === '2xs' ? 'text-[0.5rem]' : 'text-sm',
textClass
)}
@@ -89,22 +94,27 @@
? 'peer-checked:bg-red-600'
: color == 'blue'
? 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500'
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-400',
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-900',
size === 'sm'
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
: size === '2sm'
? 'w-9 h-5 after:top-0.5 after:left-[2px] after:h-4 after:w-4'
: size === '2xs'
? 'w-5 h-3 after:top-0.5 after:left-[2px] after:h-2 after:w-2'
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3',
inputBorderClass()
)}
></div>
</div>
{#if Boolean(options?.right)}
<span
class={twMerge(
'ml-2 font-medium duration-50 select-none',
bothOptions || textDisabled ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
'ml-2 font-normal duration-50 select-none',
bothOptions || textDisabled
? checked
? 'text-tertiary'
: 'text-disabled'
: 'text-tertiary',
size === 'xs' || size === '2sm' ? 'text-xs' : size === '2xs' ? 'text-xs' : 'text-sm',
textClass
)}
+2 -14
View File
@@ -244,20 +244,8 @@
class="mb-4"
>
{#snippet children({ item })}
<ToggleButton
position="left"
value="normal"
size="sm"
label="Any jobs within worker tags"
{item}
/>
<ToggleButton
position="dedicated"
value="dedicated"
size="sm"
label="Dedicated to a script/flow"
{item}
/>
<ToggleButton value="normal" small label="Any jobs within worker tags" {item} />
<ToggleButton value="dedicated" small label="Dedicated to a script/flow" {item} />
{/snippet}
</ToggleButtonGroup>
{#if selected == 'normal'}
@@ -1,5 +1,6 @@
<script lang="ts">
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import { inputBaseClass, inputBorderClass } from '$lib/components/text_input/TextInput.svelte'
import { onMount, untrack } from 'svelte'
/* Forked from MIT LICENSE
@@ -208,7 +209,7 @@
bind:value
/>
<input
class="
class="{inputBaseClass} {inputBorderClass()}
{inputClasses?.formatted ?? DEFAULT_CLASS_FORMATTED}
{isNegativeAllowed && !isZero && !isNegative
? (inputClasses?.formattedPositive ?? DEFAULT_CLASS_FORMATTED_POSITIVE)
@@ -128,7 +128,6 @@
}
</script>
<input />
<div class="w-full z-[1000] overflow-visible h-full">
<div class={$app.hideLegacyTopBar ? 'hidden' : ''}>
<div
@@ -157,7 +157,7 @@
</script>
{#if !(resourceOnly && (fieldType !== 'object' || !format?.startsWith('resource-')))}
<div class={classNames('flex gap-1', 'flex-col')}>
<div class={classNames('flex gap-1 flex-col group')}>
<div class="flex justify-between items-end">
<div class="flex flex-row gap-4 items-center">
<div class="flex items-center">
@@ -188,8 +188,14 @@
<div class={classNames('flex gap-x-2 gap-y-1 justify-end items-center')}>
{#if componentInput?.type && allowTypeChange !== false}
<ConnectionButton
{closeConnection}
{openConnection}
isOpen={!!$connectingInput.opened}
btnWrapperClasses={'h-6 w-8 opacity-0 group-hover:opacity-100 transition-opacity'}
/>
<ToggleButtonGroup
class="h-7"
class="h-6"
bind:selected={componentInput.type}
on:selected={(e) => {
if (
@@ -242,7 +248,6 @@
<ToggleButton value="evalv2" icon={FunctionSquare} iconOnly tooltip="Eval" {item} />
{/snippet}
</ToggleButtonGroup>
<ConnectionButton {closeConnection} {openConnection} isOpen={!!$connectingInput.opened} />
{/if}
</div>
</div>
@@ -0,0 +1,55 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements'
import { twMerge } from 'tailwind-merge'
type Props = {
children: import('svelte').Snippet
innerClass?: string
class?: string
horizontal?: boolean
vertical?: boolean
outerDivProps?: HTMLAttributes<HTMLDivElement>
}
let {
children,
innerClass,
class: className = '',
horizontal,
vertical,
outerDivProps
}: Props = $props()
let innerContainer: HTMLElement | null = $state(null)
let outerContainer: HTMLElement | null = $state(null)
let width: number | null = $state(null)
let height: number | null = $state(null)
let style = $derived.by(() => {
let s = 'transition: width 0.2s, height 0.2s; overflow: clip;'
if (width !== null) s += `width: ${width}px;`
if (height !== null) s += `height: ${height}px;`
return s
})
$effect(() => {
if (!innerContainer || !outerContainer) return
let observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (horizontal) width = entry.contentRect.width
if (vertical) height = entry.contentRect.height
}
})
observer.observe(innerContainer)
return () => {
if (innerContainer) observer.unobserve(innerContainer)
observer.disconnect()
}
})
</script>
<div bind:this={outerContainer} {...outerDivProps} class={className} {style}>
<div class={twMerge('absolute', innerClass)} bind:this={innerContainer}>
{@render children()}
</div>
</div>
@@ -154,7 +154,7 @@
variant === 'border' ? 'border' : '',
ButtonType.FontSizeClasses[size],
ButtonType.SpacingClasses[spacingSize][variant],
'focus-visible:ring-2 font-semibold',
'focus-visible:ring-2 font-normal',
dropdownItems && dropdownItems.length > 0 ? 'rounded-l-md h-full' : 'rounded-md',
'justify-center items-center text-center whitespace-nowrap inline-flex gap-2',
btnClasses,
@@ -349,6 +349,7 @@
on:open={() => dispatch('dropdownOpen', true)}
on:close={() => dispatch('dropdownOpen', false)}
bind:open={dropdownOpen}
enableFlyTransition
>
{#snippet buttonReplacement()}
<div
@@ -5,10 +5,12 @@
import Button from '$lib/components/common/button/Button.svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '$lib/components/apps/types'
import { twMerge } from 'tailwind-merge'
export let isOpen = false
export let openConnection: () => void
export let closeConnection: () => void
export let btnWrapperClasses = ''
let selected = false
@@ -82,6 +84,8 @@
title="Connect"
on:click={() => handleConnect(true)}
id="schema-plug"
wrapperClasses={twMerge(btnWrapperClasses, selected ? 'opacity-100' : '')}
btnClasses="p-0"
>
<Plug size={14} />
</Button>
@@ -27,7 +27,7 @@
<button
title="Open calendar picker"
class={twMerge(
'absolute bottom-1 right-2 top-1 py-1 min-w-min !px-2 items-center text-primary bg-surface border rounded center-center hover:bg-surface-hover transition-all cursor-pointer',
'absolute bottom-1 right-2 top-1 py-1 min-w-min !px-2.5 items-center text-primary bg-surface-secondary rounded center-center hover:bg-surface-hover transition-all cursor-pointer',
$$props.class
)}
aria-label="Open calendar picker"
@@ -15,7 +15,7 @@
export let multiple = false
export let convertTo: ReadFileAs | undefined = undefined
export let hideIcon = false
export let iconSize = 36
export let iconSize = 24
export let returnFileNames = false
export let submittedText: string | undefined = undefined
export let defaultFile: string | string[] | undefined = undefined
@@ -187,10 +187,11 @@
<button
class={twMerge(
`relative center-center flex-col text-center font-medium text-tertiary
border border-dashed border-gray-400 hover:border-blue-500
`relative center-center flex flex-col gap-x-2 gap-y-1 justify-center items-center flex-wrap text-center font-normal text-hint text-xs rounded-md
bg-surface-secondary
border border-dashed border-nord-400 dark:border-nord-300 hover:border-nord-900 dark:hover:border-nord-900
focus-within:border-blue-300 hover:bg-blue-50 dark:hover:bg-frost-900
duration-200 rounded-component p-1`,
duration-200 px-1 py-8`,
c
)}
on:dragover={handleDragOver}
@@ -209,7 +210,7 @@
{disabled}
>
{#if !hideIcon && !files}
<FileUp size={iconSize} class="mb-2" />
<FileUp size={iconSize} />
{/if}
{#if files}
<div class="w-full max-h-full overflow-auto px-6">
@@ -258,7 +259,7 @@
/>
{#if defaultFile && (!Array.isArray(defaultFile) || defaultFile.length > 0)}
<div class="w-full border-dashed border-t-2 text-2xs pt-1 text-tertiary mt-2">
Default file: <span class="text-blue-500">{defaultFile}</span>
Default file: <span class="text-nord-900">{defaultFile}</span>
</div>
{/if}
</button>
@@ -17,7 +17,8 @@
computeS3ForceViewerPolicies,
workspace,
editor = $bindable(),
appPath
appPath,
bottom
}: {
multiple: boolean
value: any
@@ -38,6 +39,7 @@
workspace: string | undefined
editor: SimpleEditor | undefined
appPath: string | undefined
bottom?: import('svelte').Snippet
} = $props()
let s3FileUploadRawMode: boolean = $state(false)
@@ -67,13 +69,7 @@
/>
{/if}
<div class="flex flex-col w-full gap-1">
<Toggle
class="flex justify-end"
bind:checked={s3FileUploadRawMode}
size="xs"
options={{ left: `Raw S3 object${multiple ? 's' : ''} input` }}
/>
<div class="flex flex-col w-full gap-1 relative">
{#if s3FileUploadRawMode}
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
@@ -135,7 +131,7 @@
variant="border"
color="light"
size="xs"
btnClasses="mt-1"
btnClasses="mt-1 font-normal text-tertiary"
on:click={() => {
s3FilePicker?.open?.(value)
}}
@@ -144,4 +140,12 @@
{multiple ? 'Add' : 'Choose'} an object from the catalog
</Button>
{/if}
{@render bottom?.()}
<Toggle
class="mt-1"
textClass="font-normal text-hint"
bind:checked={s3FileUploadRawMode}
size="xs"
options={{ right: `Raw S3 object${multiple ? 's' : ''} input` }}
/>
</div>
@@ -48,6 +48,7 @@
<div
use:clickOutside={{
eventToListenName: 'pointerdown',
stopPropagation: true,
onClickOutside: () => (open = false)
}}
>
@@ -4,20 +4,41 @@
import { type ToggleGroupElements, type ToggleGroupItemProps, melt } from '@melt-ui/svelte'
import { Info } from 'lucide-svelte'
export let label: string | undefined = undefined
export let iconOnly: boolean = false
export let tooltip: string | undefined = undefined
export let icon: any | undefined = undefined
export let disabled: boolean = false
export let selectedColor: string = '#3b82f6'
export let small = false
export let light = false
export let iconProps: Record<string, any> = {}
export let showTooltipIcon: boolean = false
export let documentationLink: string | undefined = undefined
export let id: string | undefined = undefined
export let item: ToggleGroupElements['item']
export let value: ToggleGroupItemProps
interface Props {
label?: string | undefined
iconOnly?: boolean
tooltip?: string | undefined
icon?: any | undefined
disabled?: boolean
selectedColor?: string | undefined
small?: boolean
light?: boolean
iconProps?: Record<string, any>
showTooltipIcon?: boolean
documentationLink?: string | undefined
id?: string | undefined
item: ToggleGroupElements['item']
value: ToggleGroupItemProps
class?: string
}
let {
label = undefined,
iconOnly = false,
tooltip = undefined,
icon = undefined,
disabled = false,
selectedColor = undefined,
small = false,
light = false,
iconProps = {},
showTooltipIcon = false,
documentationLink = undefined,
id = undefined,
item,
value,
class: className = ''
}: Props = $props()
</script>
<Tooltip
@@ -30,25 +51,28 @@
{id}
{disabled}
class={twMerge(
'group rounded-md transition-all text-xs flex gap-1 flex-row items-center',
small ? 'px-1.5 py-0.5 text-2xs' : 'px-2 py-1',
'group rounded-md transition-all font-normal flex gap-1 flex-row items-center border',
small ? 'px-1.5 py-0.5 text-2xs' : 'px-2 py-1 text-sm',
light ? 'font-medium' : '',
'data-[state=on]:bg-surface data-[state=on]:shadow-md',
'data-[state=on]:bg-surface data-[state=off]:border-transparent data-[state=on]:border-gray-300 dark:data-[state=on]:border-gray-500',
'text-hint hover:text-secondary data-[state=on]:text-secondary data-[state=on]:font-medium hover:font-medium',
'bg-surface-secondary hover:bg-surface-hover',
disabled ? '!shadow-none' : '',
$$props.class
className
)}
use:melt={$item(value)}
style={`--selected-color: ${selectedColor}`}
style={selectedColor ? `--selected-color: ${selectedColor}` : ''}
>
{#if icon}
<svelte:component
this={icon}
{@const SvelteComponent = icon}
<SvelteComponent
size={small ? 12 : 14}
{...iconProps}
class={twMerge(
'text-gray-400',
'group-data-[state=on]:text-[var(--selected-color)]',
selectedColor
? 'group-data-[state=on]:text-[var(--selected-color)]'
: 'group-data-[state=on]:text-nord-950 dark:group-data-[state=on]:text-nord-900',
iconProps.class
)}
/>
@@ -61,7 +85,7 @@
{/if}
</button>
<svelte:fragment slot="text">
{#snippet text()}
{tooltip}
</svelte:fragment>
{/snippet}
</Tooltip>
@@ -72,7 +72,7 @@
>
<div
class={twMerge(
'flex bg-surface-secondary rounded-md p-0.5 gap-1 h-full',
'flex bg-surface-secondary rounded-md h-full',
wrap ? 'flex-wrap' : '',
tabListClass
)}
@@ -18,6 +18,7 @@
item?: any | undefined
selected?: string | undefined
togglableItems: TogglableItem[]
class?: string
}
let {
@@ -27,7 +28,8 @@
id = undefined,
item = undefined,
selected = $bindable(undefined),
togglableItems
togglableItems,
class: className = ''
}: Props = $props()
let items = togglableItems.map((i) => ({
@@ -44,7 +46,7 @@
<Popover
disablePopup={true}
notClickable
class={twMerge('flex', disabled ? 'cursor-not-allowed' : 'cursor-pointer')}
class={twMerge('flex', disabled ? 'cursor-not-allowed' : 'cursor-pointer', className)}
disappearTimeout={0}
>
<div {id} class="flex">
@@ -63,7 +65,7 @@
/>
{/if}
<div class="flex items-center">
<DropdownV2 {items} />
<DropdownV2 enableFlyTransition {items} />
</div>
</div>
</Popover>
@@ -1,3 +1,14 @@
<script lang="ts" module>
export function stepInputGenButtonClasses(selected: boolean) {
return twMerge(
'text-violet-500 dark:text-violet-400 border',
selected
? 'bg-green-50 hover:bg-green-50 dark:bg-green-400/15 dark:hover:bg-green-400/15 text-green-800 border-green-200 dark:border-green-300/60 dark:text-green-400 '
: 'hover:bg-violet-50 border-violet-100 dark:hover:bg-violet-400/15 dark:border-violet-400/20'
)
}
</script>
<script lang="ts">
import { Check, Loader2, Wand2 } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
@@ -27,6 +38,7 @@
export let pickableProperties: PickableProperties | undefined = undefined
export let argName: string
export let showPopup: boolean
export let btnClass = ''
let empty = false
$: empty =
@@ -211,10 +223,8 @@ Only return the expression without any wrapper.`
size="xs"
color="light"
btnClasses={twMerge(
'text-violet-800 dark:text-violet-400 bg-violet-100 dark:bg-gray-700 dark:hover:bg-surface-hover',
!loading && generatedContent.length > 0
? 'bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-400 dark:bg-green-700 dark:hover:bg-green-700'
: ''
stepInputGenButtonClasses(!loading && generatedContent.length > 0),
btnClass
)}
on:click={() => {
if (!loading && generatedContent.length > 0) {
@@ -18,6 +18,7 @@
import FlowCopilotInputsModal from './FlowCopilotInputsModal.svelte'
import type { Flow } from '$lib/gen'
import { twMerge } from 'tailwind-merge'
import { stepInputGenButtonClasses } from './StepInputGen.svelte'
let loading = false
export let pickableProperties: PickableProperties | undefined = undefined
@@ -178,11 +179,9 @@ input_name2: expression2
<Button
size="xs"
color="light"
wrapperClasses="flex-1"
btnClasses={twMerge(
'text-violet-800 dark:text-violet-400',
!loading && Object.keys($generatedExprs || {}).length > 0
? 'bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-400 dark:bg-green-700 dark:hover:bg-green-700'
: ''
stepInputGenButtonClasses(!loading && Object.keys($generatedExprs || {}).length > 0)
)}
on:mouseenter={(ev) => {
if (out) {
@@ -219,12 +218,13 @@ input_name2: expression2
floatingConfig={{
placement: 'top-end'
}}
class="w-full"
>
<svelte:fragment slot="trigger">
<Button
size="xs"
color="light"
btnClasses="text-violet-800 dark:text-violet-400"
btnClasses="text-violet-800 dark:text-violet-400 border border-violet-200 hover:bg-violet-50"
nonCaptureEvent
startIcon={{
icon: Wand2
@@ -2,19 +2,19 @@
import Button from '$lib/components/common/button/Button.svelte'
import { ChevronDown } from 'lucide-svelte'
import { slide } from 'svelte/transition'
$: opened = false
</script>
<div class="text-xs flex flex-row-reverse mt-1">
<div class="text-xs flex flex-row-reverse">
<Button
on:click={() => {
opened = !opened
}}
color="light"
variant="divider"
size="xs2"
endIcon={{ icon: ChevronDown, classes: `rotate-0 duration-300 ${opened ? '!rotate-180' : ''}` }}
btnClasses="text-hint font-normal pt-1"
>
Help
</Button>
@@ -22,8 +22,7 @@
{#if opened}
<div
transition:slide|local
class="bg-surface-secondary border-l-4 text-secondary p-4 m-4"
class="bg-surface-secondary border-l-4 text-sm text-secondary p-4 mt-2"
role="alert"
id="dynamic-input-help-box"
>
@@ -572,7 +572,7 @@
></div>
{/if}
<InputTransformSchemaForm
class="px-1 xl:px-2"
class="px-2 xl:px-4"
bind:this={inputTransformSchemaForm}
pickableProperties={stepPropPicker.pickableProperties}
schema={flowStateStore.val[$selectedId]?.schema ?? {}}
@@ -852,19 +852,14 @@
{#if flowModule.value['language'] === 'deno'}
<ToggleButton
value="s3_client"
size="sm"
small
label="S3 lite client"
{item}
/>
{:else}
<ToggleButton
value="s3_client"
size="sm"
label="Boto3"
{item}
/>
<ToggleButton value="polars" size="sm" label="Polars" {item} />
<ToggleButton value="duckdb" size="sm" label="DuckDB" {item} />
<ToggleButton value="s3_client" small label="Boto3" {item} />
<ToggleButton value="polars" small label="Polars" {item} />
<ToggleButton value="duckdb" small label="DuckDB" {item} />
{/if}
{/snippet}
</ToggleButtonGroup>
@@ -108,11 +108,7 @@
>
<slot />
</Pane>
<Pane
minSize={20}
size={40}
class="!transition-none z-1000 {$propPickerConfig ? 'ml-[-1px]' : ''} {paneClass}"
>
<Pane minSize={20} size={40} class="!transition-none z-1000 {paneClass}">
<AnimatedButton
animate={$propPickerConfig?.insertionMode == 'connect'}
baseRadius="4px"
@@ -158,14 +154,3 @@
</Pane>
</Splitpanes>
</div>
<style>
:global(.splitpanes-remove-splitter > .splitpanes__pane) {
background-color: inherit !important;
}
:global(.splitpanes-remove-splitter > .splitpanes__splitter) {
background-color: transparent !important;
width: 0 !important;
border: none !important;
}
</style>
@@ -12,10 +12,10 @@
let { label, children, tooltip, for: forAttr = '', noLabel = false }: Props = $props()
</script>
<div class="flex flex-col items-start w-fit">
<div class="flex flex-col items-start w-fit gap-0.5">
<div class="text-xs truncate">
{#if forAttr}
<label for={forAttr} class:text-transparent={noLabel}>{label}</label>
<label for={forAttr} class="text-hint" class:text-transparent={noLabel}>{label}</label>
{:else}
<span class:text-transparent={noLabel}>{label}</span>
{/if}
@@ -183,9 +183,10 @@
</RunOption>
{/if}
<!-- Filter by -->
<div class="flex flex-row gap-1">
<div class="flex flex-row gap-4">
<RunOption label="Filter by" for="filter-by">
<ToggleButtonGroup
class="h-9"
bind:selected={filterBy}
on:selected={(e) => {
if (e.detail != filterBy) {
@@ -226,7 +227,7 @@
bind:value={() => user ?? undefined, (v) => (user = v ?? null)}
clearable
onClear={() => ((user = null), dispatch('reset'))}
inputClass="!h-[32px] min-w-36"
inputClass="!max-h-9 min-w-36 border-none"
onCreateItem={(item) => (usernames.push(item), (user = item))}
createText="Press enter to use this value"
id="user"
@@ -241,7 +242,7 @@
bind:value={() => folder ?? undefined, (v) => (folder = v ?? null)}
clearable
onClear={() => ((folder = null), dispatch('reset'))}
inputClass="!h-[32px] min-w-36"
inputClass="!max-h-9 min-w-36 border-none"
id="folder"
/>
{/key}
@@ -254,7 +255,7 @@
bind:value={() => path ?? undefined, (v) => (path = v ?? null)}
clearable
onClear={() => ((path = null), dispatch('reset'))}
inputClass="!h-[32px] min-w-36"
inputClass="!max-h-9 min-w-36 border-none"
onCreateItem={(item) => (paths.push(item), (path = item))}
createText="Press enter to use this value"
id="path"
@@ -479,7 +480,7 @@
<RunOption label="Kind" for="kind">
{#if small && !calendarSmall}
<DropdownSelect
btnClasses="min-w-24"
btnClasses="min-w-24 h-9 bg-surface-secondary font-normal"
items={[
{
displayName: 'All',
@@ -524,7 +525,7 @@
selected={jobKindsCat}
/>
{:else}
<ToggleButtonGroup bind:selected={jobKindsCat}>
<ToggleButtonGroup bind:selected={jobKindsCat} class="h-9">
{#snippet children({ item })}
<ToggleButton value="all" label="All" {item} />
<ToggleButton
@@ -571,6 +572,7 @@
<!-- Status -->
<RunOption label="Status" for="status">
<ToggleButtonGroup
class="h-9"
selected={success ?? 'all'}
on:selected={({ detail }) => {
success = detail === 'all' ? undefined : detail
@@ -638,11 +640,12 @@
{#snippet trigger()}
<Button
color="light"
variant="border"
variant="contained"
size="xs"
nonCaptureEvent={true}
startIcon={{ icon: ListFilterPlus }}
iconOnly
btnClasses="bg-surface-secondary py-[8.5px]"
></Button>
{/snippet}
@@ -444,9 +444,10 @@
showExpr = showExpr ? undefined : 'true //fields.foo == 42'
}}
/>
{#if showExpr != undefined}
<div class="border">
{#if showExpr}
<div class="mt-2 bg-surface-secondary rounded-md pl-4">
<SimpleEditor
yPadding={8}
extraLib={`declare const fields: Record<${propsNames
?.filter((x) => x != name)
.map((x) => `"${x}"`)
@@ -455,6 +456,7 @@
bind:code={showExpr}
shouldBindKey={false}
fixedOverflowWidgets={false}
hideLineNumbers
autoHeight
/>
</div>
@@ -16,6 +16,7 @@
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import { createEventDispatcher, onMount, untrack } from 'svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
import TextInput, { inputBaseClass, inputBorderClass } from '../text_input/TextInput.svelte'
interface Props {
description?: string
@@ -179,6 +180,7 @@
onkeydown={onKeyDown}
onchange={() => dispatch('change')}
placeholder="Field description"
class={twMerge(inputBorderClass(), inputBaseClass, 'w-full')}
></textarea>
</Label>
@@ -186,30 +188,31 @@
{#snippet header()}
<Tooltip light>Will be displayed in the UI instead of the field name.</Tooltip>
{/snippet}
<input
<TextInput
bind:value={title}
onchange={() => dispatch('change')}
onkeydown={onKeyDown}
placeholder="Field title"
inputProps={{ placeholder: 'Field title', onkeydown, onchange: () => dispatch('change') }}
/>
</Label>
<Label label="Placeholder">
{#snippet header()}
<Tooltip light>
Will be displayed in the input field when the field is empty. If not set, the default
value will be used. The placeholder is disabled depending on the field type, format, etc.
</Tooltip>
{/snippet}
{#if shouldDisplayPlaceholder(type, format, enum_, contentEncoding, pattern, extra)}
<Label label="Placeholder">
{#snippet header()}
<Tooltip light>
Will be displayed in the input field when the field is empty. If not set, the default
value will be used. The placeholder is disabled depending on the field type, format,
etc.
</Tooltip>
{/snippet}
<textarea
placeholder="Enter a placeholder"
rows="1"
bind:value={placeholder}
onchange={() => dispatch('change')}
disabled={!shouldDisplayPlaceholder(type, format, enum_, contentEncoding, pattern, extra)}
></textarea>
</Label>
<textarea
placeholder="Enter a placeholder"
rows="1"
bind:value={placeholder}
onchange={() => dispatch('change')}
class={twMerge(inputBorderClass(), inputBaseClass, 'w-full')}
></textarea>
</Label>
{/if}
{#if type == 'array'}
<ArrayTypeNarrowing
@@ -285,11 +288,3 @@
{@render children?.()}
</div>
</div>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
</style>
@@ -25,6 +25,7 @@
helperScript?: DynamicInput.HelperScript
className?: string
dndType?: string
lightHeaderFont?: boolean
}
let {
@@ -44,7 +45,8 @@
isValid = $bindable(true),
noVariablePicker = false,
className = '',
dndType = generateRandomString()
dndType = generateRandomString(),
lightHeaderFont
}: Props = $props()
$effect.pre(() => {
@@ -135,10 +137,11 @@
{shouldDispatchChanges}
bind:isValid
{noVariablePicker}
{lightHeaderFont}
>
{#snippet actions()}
{#if !disableDnd}
<div class="w-4 h-8 cursor-move ml-2 handle" aria-label="drag-handle" use:dragHandle>
<div class="w-4 h-8 cursor-move ml-2 handle mt-[9px]" aria-label="drag-handle" use:dragHandle>
<GripVertical size={16} />
</div>
{/if}
@@ -30,7 +30,7 @@
role="listitem"
class={twMerge(
allowClear ? 'pr-1' : 'pr-3',
'pl-3 min-h-6 bg-surface-secondary rounded-full flex items-center gap-0.5',
'pl-3 min-h-6 border bg-surface rounded-full flex items-center gap-0.5',
currentlyDraggingIndex !== undefined ? 'hover:opacity-20' : ''
)}
style={currentlyDraggingIndex === index
@@ -54,7 +54,7 @@
<span class="text-sm select-none">{item.label || item.value}</span>
{#if allowClear}
<CloseButton
class="text-tertiary"
class="text-hint bg-transparent"
small
on:close={(e) => (onRemove(item), e.stopPropagation())}
/>
@@ -7,6 +7,7 @@
import DraggableTags from './DraggableTags.svelte'
import { Search } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import TextInput, { inputBorderClass } from '../text_input/TextInput.svelte'
type Value = Item['value']
@@ -59,7 +60,7 @@
let filterText = $state<string>('')
let open = $state<boolean>(false)
let wrapperEl: HTMLDivElement | undefined = $state()
let searchInputEl: HTMLInputElement | undefined = $state()
let searchInputEl: TextInput | undefined = $state()
$effect(() => searchInputEl?.focus())
@@ -99,7 +100,8 @@
<div
bind:this={wrapperEl}
class={twMerge(
'relative min-h-8 flex items-center w-full bg-surface border border-gray-300 rounded-md text-tertiary',
'relative min-h-10 flex items-center w-full bg-surface-secondary !text-secondary rounded-md',
inputBorderClass({ forceFocus: open && !disabled }),
disabled ? 'pointer-events-none' : '',
open && !disabled ? 'open' : '',
disabled ? 'disabled' : '',
@@ -113,7 +115,9 @@
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
{#if value.length === 0}
<span class={twMerge('text-sm ml-2 h-full flex items-center flex-1', placeholderClass)}>
<span
class={twMerge('text-sm ml-4 h-full flex items-center flex-1 text-hint', placeholderClass)}
>
{placeholder}
</span>
{:else}
@@ -134,10 +138,10 @@
/>
</ul>
{/if}
{#if allowClear && !hideMainClearBtn}
{#if allowClear && !hideMainClearBtn && !!value?.length}
<CloseButton
noBg
class="mr-1 remove-all"
class="mr-1 remove-all bg-transparent text-hint"
small
on:close={(e) => (clearValue(), e.stopPropagation())}
/>
@@ -163,12 +167,14 @@
{#snippet header()}
{#if processedItems.length - value.length > 0 || onCreateItem}
<div class="mx-2 mb-1 mt-2 flex items-center relative">
<input
<TextInput
bind:this={searchInputEl}
bind:value={filterText}
onblur={(e) => (e.preventDefault(), searchInputEl?.focus())}
placeholder="Search"
class="!pr-7"
inputProps={{
onblur: (e) => (e.preventDefault(), searchInputEl?.focus()),
placeholder: 'Search...'
}}
class="!pr-7 !bg-surface"
/>
<Search size={16} class="absolute right-2 text-tertiary" />
</div>
@@ -7,6 +7,7 @@
import { getLabel, processItems, type ProcessedItem } from './utils.svelte'
import SelectDropdown from './SelectDropdown.svelte'
import { deepEqual } from 'fast-equals'
import { inputBaseClass, inputBorderClass } from '../text_input/TextInput.svelte'
type Value = Item['value']
@@ -23,19 +24,24 @@
inputClass = '',
disablePortal = false,
loading = false,
error = false,
autofocus,
RightIcon,
createText,
noItemsMsg,
open = $bindable(false),
id,
itemLabelWrapperClasses,
itemButtonWrapperClasses,
groupBy,
sortBy,
onFocus,
onBlur,
onClear,
onCreateItem,
startSnippet
startSnippet,
endSnippet,
bottomSnippet
}: {
items?: Item[]
value: Value | undefined
@@ -49,19 +55,24 @@
inputClass?: string
disablePortal?: boolean
loading?: boolean
error?: boolean
autofocus?: boolean
RightIcon?: any
createText?: string
noItemsMsg?: string
open?: boolean
id?: string
itemLabelWrapperClasses?: string
itemButtonWrapperClasses?: string
groupBy?: (item: Item) => string
sortBy?: (a: Item, b: Item) => number
onFocus?: () => void
onBlur?: () => void
onClear?: () => void
onCreateItem?: (value: string) => void
startSnippet?: Snippet<[{ item: ProcessedItem<Value> }]>
startSnippet?: Snippet<[{ item: ProcessedItem<Value>; close: () => void }]>
endSnippet?: Snippet<[{ item: ProcessedItem<Value>; close: () => void }]>
bottomSnippet?: Snippet<[{ close: () => void }]>
} = $props()
let disabled = $derived(_disabled || (loading && !value))
@@ -112,7 +123,7 @@
</div>
{:else if clearable && !disabled && value}
<div class="absolute z-10 right-2 h-full flex items-center">
<CloseButton class="text-secondary" noBg small on:close={clearValue} />
<CloseButton class="bg-transparent text-hint" noBg small on:close={clearValue} />
</div>
{:else if RightIcon}
<div class="absolute z-10 right-2 h-full flex items-center">
@@ -130,10 +141,12 @@
: (valueEntry?.label ?? getLabel({ value }) ?? placeholder)}
style={containerStyle}
class={twMerge(
'!bg-surface text-ellipsis',
inputBaseClass,
inputBorderClass({ error, forceFocus: open }),
'w-full',
open ? '' : 'cursor-pointer',
!loading && value ? '!placeholder-primary' : '',
(clearable || RightIcon) && !disabled && value ? '!pr-8' : '',
!loading && value && !disabled ? '!placeholder-secondary' : 'placeholder-hint',
(clearable || RightIcon) && !disabled && value ? 'pr-8' : '',
inputClass ?? ''
)}
autocomplete="off"
@@ -152,6 +165,10 @@
getInputRect={inputEl && (() => inputEl!.getBoundingClientRect())}
{listAutoWidth}
{noItemsMsg}
{itemLabelWrapperClasses}
{itemButtonWrapperClasses}
{startSnippet}
{endSnippet}
{bottomSnippet}
/>
</div>
@@ -4,6 +4,7 @@
import { untrack, type Snippet } from 'svelte'
import type { ProcessedItem } from './utils.svelte'
import { twMerge } from 'tailwind-merge'
import { slide } from 'svelte/transition'
let {
processedItems: _processedItems,
@@ -16,10 +17,14 @@
noItemsMsg = 'No items found',
class: className = '',
ulClass = '',
itemLabelWrapperClasses = '',
itemButtonWrapperClasses = '',
header,
getInputRect,
onSelectValue,
startSnippet
startSnippet,
endSnippet,
bottomSnippet
}: {
processedItems?: ProcessedItem<T>[]
value: T | undefined
@@ -31,10 +36,14 @@
noItemsMsg?: string
class?: string
ulClass?: string
itemLabelWrapperClasses?: string
itemButtonWrapperClasses?: string
header?: Snippet
getInputRect?: () => DOMRect
onSelectValue: (item: ProcessedItem<T>) => void
startSnippet?: Snippet<[{ item: ProcessedItem<T> }]>
startSnippet?: Snippet<[{ item: ProcessedItem<T>; close: () => void }]>
endSnippet?: Snippet<[{ item: ProcessedItem<T>; close: () => void }]>
bottomSnippet?: Snippet<[{ close: () => void }]>
} = $props()
let processedItems = $derived(
@@ -101,53 +110,60 @@
<ConditionalPortal condition={!disablePortal} name="select-dropdown-portal">
{#if open && !disabled}
<div
transition:slide={{ duration: 150 }}
class={twMerge(
disablePortal ? 'absolute' : 'fixed',
'flex flex-col z-[5001] max-h-64 overflow-y-auto bg-surface-secondary text-tertiary text-sm select-none border rounded-lg shadow-lg',
'z-[5001] rounded-md bg-surface-secondary text-tertiary shadow-lg text-sm select-none',
className
)}
style="{`top: ${dropdownPos.y}px; left: ${dropdownPos.x}px;`} {listAutoWidth
? `min-width: ${dropdownPos.width}px;`
: ''}"
bind:this={listEl}
>
{@render header?.()}
{#if processedItems?.length === 0}
<div class="py-8 px-4 text-center text-primary">{noItemsMsg}</div>
{/if}
<ul class={twMerge('flex-1 overflow-y-auto flex flex-col', ulClass)}>
{#each processedItems ?? [] as item, itemIndex}
{#if (item.__select_group && itemIndex === 0) || processedItems?.[itemIndex - 1]?.__select_group !== item.__select_group}
<li
class={twMerge(
'mx-4 pb-1 mb-2 text-xs font-semibold text-primary border-b',
itemIndex === 0 ? 'mt-3' : 'mt-6'
)}
>
{item.__select_group}
<div bind:this={listEl} class="flex flex-col max-h-64 border rounded-md overflow-clip">
{@render header?.()}
{#if processedItems?.length === 0}
<div class="py-8 px-4 text-center text-primary">{noItemsMsg}</div>
{/if}
<ul class={twMerge('flex-1 overflow-y-auto flex flex-col', ulClass)}>
{#each processedItems ?? [] as item, itemIndex}
{#if (item.__select_group && itemIndex === 0) || processedItems?.[itemIndex - 1]?.__select_group !== item.__select_group}
<li
class={twMerge(
'mx-4 pb-1 mb-2 text-xs font-semibold text-primary border-b',
itemIndex === 0 ? 'mt-3' : 'mt-6'
)}
>
{item.__select_group}
</li>
{/if}
<li>
<button
class={twMerge(
'py-2 px-4 w-full font-normal text-left text-primary',
itemIndex === keyArrowPos ? 'bg-surface-hover' : '',
item.value === value ? 'bg-surface-selected' : 'hover:bg-surface-hover',
itemButtonWrapperClasses
)}
onclick={(e) => {
e.stopImmediatePropagation()
onSelectValue(item)
}}
>
{@render startSnippet?.({ item, close: () => (open = false) })}
<span class={itemLabelWrapperClasses}>
{item.label || '\xa0'}
</span>
{@render endSnippet?.({ item, close: () => (open = false) })}
{#if item.subtitle}
<div class="text-xs text-tertiary">{item.subtitle}</div>
{/if}
</button>
</li>
{/if}
<li>
<button
class={twMerge(
'py-2 px-4 w-full font-normal text-left text-primary',
itemIndex === keyArrowPos ? 'bg-surface-hover' : '',
item.value === value ? 'bg-surface-selected' : 'hover:bg-surface-hover'
)}
onclick={(e) => {
e.stopImmediatePropagation()
onSelectValue(item)
}}
>
{@render startSnippet?.({ item })}
{item.label || '\xa0'}
{#if item.subtitle}
<div class="text-xs text-tertiary">{item.subtitle}</div>
{/if}
</button>
</li>
{/each}
</ul>
{/each}
</ul>
{@render bottomSnippet?.({ close: () => (open = false) })}
</div>
</div>
{/if}
</ConditionalPortal>
@@ -5,13 +5,21 @@
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import Toggle from '../Toggle.svelte'
import { FlowService, IntegrationService, ScriptService, UserService, type NewToken } from '$lib/gen'
import {
FlowService,
IntegrationService,
ScriptService,
UserService,
type NewToken
} from '$lib/gen'
import MultiSelect from '../select/MultiSelect.svelte'
import { safeSelectItems } from '../select/utils.svelte'
import TokenDisplay from './TokenDisplay.svelte'
import ScopeSelector from './ScopeSelector.svelte'
import Alert from '../common/alert/Alert.svelte'
import FolderPicker from '../FolderPicker.svelte'
import TextInput from '../text_input/TextInput.svelte'
import Select from '../select/Select.svelte'
interface Props {
showMcpMode?: boolean
@@ -44,7 +52,7 @@
let loadingRunnables = $state(false)
let includedRunnables = $state<string[]>([])
let selectedFolder = $state<string>('')
let runnablesCache = new Map<string, string[]>()
let customScopes = $state<string[]>([])
@@ -110,7 +118,11 @@
const workspaces = $derived(ensureCurrentWorkspaceIncluded($userWorkspaces, $workspaceStore))
const mcpBaseUrl = $derived(`${window.location.origin}/api/mcp/w/${newTokenWorkspace}/sse?token=`)
const warning = $derived(newMcpScope === 'favorites' ? `You do not have any favorite scripts or flows. You can favorite some scripts and flows to include them, or change the scope to "All scripts/flows" to include all your scripts and flows.` : 'Create your first scripts or flows to make them available via MCP.')
const warning = $derived(
newMcpScope === 'favorites'
? `You do not have any favorite scripts or flows. You can favorite some scripts and flows to include them, or change the scope to "All scripts/flows" to include all your scripts and flows.`
: 'Create your first scripts or flows to make them available via MCP.'
)
const noScriptsOrFlowsAvailableWarning = $derived(includedRunnables.length === 0 ? warning : '')
$effect(() => {
@@ -141,7 +153,11 @@
}
}
async function getScripts(favoriteOnly: boolean = false, workspace: string, folder: string | undefined) {
async function getScripts(
favoriteOnly: boolean = false,
workspace: string,
folder: string | undefined
) {
if (!workspace) {
return []
}
@@ -154,7 +170,11 @@
return scripts.map((x) => x.path)
}
async function getFlows(favoriteOnly: boolean = false, workspace: string, folder: string | undefined) {
async function getFlows(
favoriteOnly: boolean = false,
workspace: string,
folder: string | undefined
) {
if (!workspace) {
return []
}
@@ -167,7 +187,11 @@
return flows.map((x) => x.path)
}
async function getScriptsAndFlows(favoriteOnly: boolean = false, workspace: string, folder: string | undefined) {
async function getScriptsAndFlows(
favoriteOnly: boolean = false,
workspace: string,
folder: string | undefined
) {
const cacheKey = `${workspace}-${favoriteOnly}${folder ? `-${folder}` : ''}`
if (runnablesCache.has(cacheKey)) {
includedRunnables = runnablesCache.get(cacheKey) || []
@@ -176,7 +200,10 @@
try {
loadingRunnables = true
const [scripts, flows] = await Promise.all([getScripts(favoriteOnly, workspace, folder), getFlows(favoriteOnly, workspace, folder)])
const [scripts, flows] = await Promise.all([
getScripts(favoriteOnly, workspace, folder),
getFlows(favoriteOnly, workspace, folder)
])
const combined = [...scripts, ...flows]
runnablesCache.set(cacheKey, combined)
includedRunnables = combined
@@ -333,7 +360,11 @@
<div>
<span class="block mb-1">Label <span class="text-xs text-tertiary">(optional)</span></span>
<input type="text" bind:value={newTokenLabel} class="w-full" />
<TextInput
inputProps={{ type: 'text' }}
bind:value={newTokenLabel}
class="w-full !bg-surface"
/>
</div>
{#if !mcpCreationMode}
@@ -341,58 +372,61 @@
<span class="block mb-1"
>Expires In <span class="text-xs text-tertiary">(optional)</span></span
>
<select bind:value={newTokenExpiration} class="w-full">
<option value={undefined}>No expiration</option>
<option value={15 * 60}>15m</option>
<option value={30 * 60}>30m</option>
<option value={1 * 60 * 60}>1h</option>
<option value={1 * 24 * 60 * 60}>1d</option>
<option value={7 * 24 * 60 * 60}>7d</option>
<option value={30 * 24 * 60 * 60}>30d</option>
<option value={90 * 24 * 60 * 60}>90d</option>
</select>
<Select
bind:value={newTokenExpiration}
placeholder="No expiration"
inputClass="!bg-surface"
items={[
{ label: 'No expiration', value: undefined },
{ label: '15 minutes', value: 15 * 60 },
{ label: '30 minutes', value: 30 * 60 },
{ label: '1 hour', value: 1 * 60 * 60 },
{ label: '1 day', value: 1 * 24 * 60 * 60 },
{ label: '7 days', value: 7 * 24 * 60 * 60 },
{ label: '30 days', value: 30 * 24 * 60 * 60 },
{ label: '90 days', value: 90 * 24 * 60 * 60 }
]}
/>
</div>
{/if}
{#if mcpCreationMode && (newMcpScope !== 'folder' || selectedFolder.length > 0)}
{#if loadingRunnables}
<div class="flex flex-col gap-2 col-span-2 pr-4">
<span class="block text-xs text-tertiary">Scripts & Flows that will be available via MCP</span>
<div class="flex flex-wrap gap-1">
<Badge rounded small color="dark-gray" baseClass="animate-skeleton">Loading...</Badge>
</div>
</div>
{:else}
<div class="flex flex-col gap-2 col-span-2 pr-4">
{#if noScriptsOrFlowsAvailableWarning}
<Alert type="info" title="No scripts or flows available" size="xs">
{noScriptsOrFlowsAvailableWarning}
</Alert>
{:else}
<span class="block text-xs text-tertiary">Scripts & Flows that will be available via MCP</span>
{#if loadingRunnables}
<div class="flex flex-col gap-2 col-span-2 pr-4">
<span class="block text-xs text-tertiary"
>Scripts & Flows that will be available via MCP</span
>
<div class="flex flex-wrap gap-1">
{#if includedRunnables.length <= 5}
{#each includedRunnables as scriptOrFlow}
<Badge rounded small color="blue">{scriptOrFlow}</Badge>
{/each}
{:else}
{#each includedRunnables.slice(0, 3) as scriptOrFlow}
<Badge rounded small color="blue">{scriptOrFlow}</Badge>
{/each}
<Badge
rounded
small
color="dark-gray"
>
+{includedRunnables.length - 3} more
</Badge>
{/if}
<Badge rounded small color="dark-gray" baseClass="animate-skeleton">Loading...</Badge>
</div>
{/if}
</div>
</div>
{:else}
<div class="flex flex-col gap-2 col-span-2 pr-4">
{#if noScriptsOrFlowsAvailableWarning}
<Alert type="info" title="No scripts or flows available" size="xs">
{noScriptsOrFlowsAvailableWarning}
</Alert>
{:else}
<span class="block text-xs text-tertiary"
>Scripts & Flows that will be available via MCP</span
>
<div class="flex flex-wrap gap-1">
{#if includedRunnables.length <= 5}
{#each includedRunnables as scriptOrFlow}
<Badge rounded small color="blue">{scriptOrFlow}</Badge>
{/each}
{:else}
{#each includedRunnables.slice(0, 3) as scriptOrFlow}
<Badge rounded small color="blue">{scriptOrFlow}</Badge>
{/each}
<Badge rounded small color="dark-gray">
+{includedRunnables.length - 3} more
</Badge>
{/if}
</div>
{/if}
</div>
{/if}
{/if}
{/if}
</div>
<div class="mt-4 flex justify-end gap-2 flex-row">
@@ -405,7 +439,8 @@
</Button>
<Button
on:click={() => createToken(mcpCreationMode)}
disabled={mcpCreationMode && (newTokenWorkspace == undefined || (newMcpScope === 'folder' && !selectedFolder))}
disabled={mcpCreationMode &&
(newTokenWorkspace == undefined || (newMcpScope === 'folder' && !selectedFolder))}
>
New token
</Button>
@@ -36,7 +36,7 @@
let nbDisplayed = $state(30)
// Instance group auto-add settings
let instanceGroups: Array<{name: string, summary?: string, emails?: string[]}> = $state([])
let instanceGroups: Array<{ name: string; summary?: string; emails?: string[] }> = $state([])
let autoAddInstanceGroups: string[] = $state([])
let autoAddInstanceGroupsRoles: Record<string, string> = $state({})
@@ -47,8 +47,8 @@
// Available groups for dropdowns - filter out already configured groups
let availableGroupItems = $derived(
instanceGroups
.filter(group => !autoAddInstanceGroups.includes(group.name))
.map(group => ({
.filter((group) => !autoAddInstanceGroups.includes(group.name))
.map((group) => ({
value: group.name,
label: group.name + (group.summary ? ` - ${group.summary}` : '')
}))
@@ -65,7 +65,10 @@
})
let hasNonManualUsers = $derived(
(filteredUsers || users || []).some((user: User) => user.added_via?.source === 'instance_group' || user.added_via?.source === 'domain')
(filteredUsers || users || []).some(
(user: User) =>
user.added_via?.source === 'instance_group' || user.added_via?.source === 'domain'
)
)
// Function to check if a manual user can be converted to a group user
@@ -78,7 +81,7 @@
// Check if user's email is in any configured instance group
const userEmail = user.email
for (const groupName of autoAddInstanceGroups) {
const group = instanceGroups.find(g => g.name === groupName)
const group = instanceGroups.find((g) => g.name === groupName)
if (group && group.emails && group.emails.includes(userEmail)) {
return true
}
@@ -87,7 +90,6 @@
return false
}
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
auto_invite_domain = settings.auto_invite_domain
@@ -177,7 +179,7 @@
await saveInstanceGroupSettings()
} catch (e) {
// Rollback on error
autoAddInstanceGroups = autoAddInstanceGroups.filter(g => g !== groupToAdd)
autoAddInstanceGroups = autoAddInstanceGroups.filter((g) => g !== groupToAdd)
delete autoAddInstanceGroupsRoles[groupToAdd]
sendUserToast('Failed to add instance group', true)
}
@@ -188,7 +190,7 @@
const previousRole = autoAddInstanceGroupsRoles[groupName]
try {
autoAddInstanceGroups = autoAddInstanceGroups.filter(g => g !== groupName)
autoAddInstanceGroups = autoAddInstanceGroups.filter((g) => g !== groupName)
delete autoAddInstanceGroupsRoles[groupName]
await saveInstanceGroupSettings()
} catch (e) {
@@ -368,8 +370,8 @@
}}
>
{#snippet children({ item })}
<ToggleButton value="invite" size="xs" label="Auto-invite" {item} />
<ToggleButton value="add" size="xs" label="Auto-add" {item} />
<ToggleButton value="invite" small label="Auto-invite" {item} />
<ToggleButton value="add" small label="Auto-add" {item} />
{/snippet}
</ToggleButtonGroup>
{/if}
@@ -469,9 +471,7 @@
{#snippet content()}
<div class="flex flex-col p-4 min-w-[500px]">
<div class="flex flex-col gap-4">
<span class="text-sm leading-6 font-semibold">
Auto-add instance groups
</span>
<span class="text-sm leading-6 font-semibold"> Auto-add instance groups </span>
<!-- Add new instance group form -->
{#if availableGroupItems.length > 0}
@@ -547,7 +547,7 @@
</thead>
<tbody>
{#each autoAddInstanceGroups as groupName (groupName)}
{@const group = instanceGroups.find(g => g.name === groupName)}
{@const group = instanceGroups.find((g) => g.name === groupName)}
<tr class="border-t border-gray-200 dark:border-gray-700">
<td class="py-2">
<div class="font-medium">{groupName}</div>
@@ -647,7 +647,8 @@
<Cell head>
Added via
<Tooltip>
Shows how the user was added to the workspace: manually, via domain auto-invite, or through an instance group.
Shows how the user was added to the workspace: manually, via domain auto-invite, or
through an instance group.
</Tooltip>
</Cell>
{/if}
@@ -674,9 +675,7 @@
{#if hasNonManualUsers && index > 0 && sortedUsers()[index - 1]?.added_via?.source !== 'instance_group' && added_via?.source === 'instance_group'}
<tr class="bg-surface-secondary">
<td colspan={hasNonManualUsers ? 8 : 7} class="px-4 py-2">
<div class="text-xs text-tertiary font-bold">
Instance group users
</div>
<div class="text-xs text-tertiary font-bold"> Instance group users </div>
</td>
</tr>
{/if}
@@ -807,7 +806,10 @@
>
Remove
</Button>
<Tooltip>Cannot remove users synced from instance groups. Either disable the user or remove them from the SCIM group.</Tooltip>
<Tooltip
>Cannot remove users synced from instance groups. Either disable the user or
remove them from the SCIM group.</Tooltip
>
</div>
{:else if canConvertToGroup(user)}
<Button
@@ -1028,7 +1030,10 @@
}}
>
<div class="flex flex-col w-full space-y-4">
<span>Are you sure you want to remove this instance group from auto-add? This will not remove users already added from this group.</span>
<span
>Are you sure you want to remove this instance group from auto-add? This will not remove
users already added from this group.</span
>
</div>
</ConfirmationModal>
</div>
@@ -0,0 +1,55 @@
<script module lang="ts">
export function inputBorderClass({
error,
forceFocus
}: {
error?: boolean
forceFocus?: boolean
} = {}) {
return twMerge(
'transition-colors border',
forceFocus
? '!border-nord-900 dark:!border-nord-900'
: '!border-nord-400 dark:!border-nord-300 hover:!border-nord-900/50 hover:dark:!border-nord-900/50 focus:!border-nord-900 dark:focus:!border-nord-900',
error
? '!border-red-300 focus:!border-red-400 hover:!border-red-500 dark:!border-red-400/40 dark:hover:!border-red-600/40'
: ''
)
}
export const inputBaseClass =
'rounded-md focus:ring-0 no-default-style text-sm text-tertiary dark:text-secondary !bg-surface-secondary disabled:!bg-surface-disabled disabled:!border-none disabled:!text-hint disabled:cursor-not-allowed shadow-none py-2 px-3 placeholder-hint'
</script>
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements'
import { twMerge } from 'tailwind-merge'
type Props = {
inputProps?: HTMLInputAttributes
value?: string
class?: string
error?: string
}
export function focus() {
inputEl?.focus()
}
let inputEl: HTMLInputElement | undefined = $state()
let { inputProps, value = $bindable(), class: className = '', error }: Props = $props()
</script>
<input
{...inputProps}
class={twMerge(
inputBaseClass,
'w-full',
'[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none',
inputBorderClass({ error: !!error }),
className
)}
bind:this={inputEl}
bind:value
/>
@@ -377,7 +377,7 @@
{/key}
{:else}
<span class="text-sm text-tertiary text-center mx-auto mt-2"
>{`Select a trigger from the ${useVerticalTriggerBar ? 'left toolbar' : 'table'} or a create a new one`}</span
>{`Select a trigger from the ${useVerticalTriggerBar ? 'left toolbar' : 'table'} or create a new one`}</span
>
{/if}
</div>
+6 -3
View File
@@ -231,7 +231,7 @@ export async function initializeVscode(caller?: string, htmlContainer?: HTMLElem
],
colors: {
'editor.foreground': '#D8DEE9',
'editor.background': '#272D38',
'editor.background': '#00000000',
'editor.selectionBackground': '#434C5ECC',
'editor.lineHighlightBackground': '#3B4252',
'editorCursor.foreground': '#D8DEE9',
@@ -244,8 +244,11 @@ export async function initializeVscode(caller?: string, htmlContainer?: HTMLElem
inherit: true,
rules: [],
colors: {
'editorLineNumber.foreground': '#999',
'editorGutter.background': '#F9FAFB'
'editor.background': '#FFFFFF00',
'editor.foreground': '#2d3748',
'editorLineNumber.foreground': '#C2C9D1',
'editorLineNumber.activeForeground': '#989DA5',
'editorGutter.background': '#FFFFFF00'
}
})
@@ -105,7 +105,8 @@
code_completion_model,
default_model,
custom_prompts: Object.keys(custom_prompts).length > 0 ? custom_prompts : undefined,
max_tokens_per_model: Object.keys(maxTokensPerModel).length > 0 ? maxTokensPerModel : undefined
max_tokens_per_model:
Object.keys(maxTokensPerModel).length > 0 ? maxTokensPerModel : undefined
}
await WorkspaceService.editCopilotConfig({
workspace: $workspaceStore!,
@@ -228,9 +229,9 @@
: provider}
initialValue={aiProviders[provider].resource_path}
bind:value={
() => aiProviders[provider].resource_path,
() => aiProviders[provider].resource_path || undefined,
(v) => {
aiProviders[provider].resource_path = v
aiProviders[provider].resource_path = v ?? ''
onAiProviderChange(provider as AIProvider)
}
}
@@ -399,8 +399,8 @@
}}
>
{#snippet children({ item })}
<ToggleButton value="operator" size="xs" label="Operator" {item} />
<ToggleButton value="developer" size="xs" label="Developer" {item} />
<ToggleButton value="operator" small label="Operator" {item} />
<ToggleButton value="developer" small label="Developer" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -77,6 +77,7 @@
import Alert from '../common/alert/Alert.svelte'
import { deepEqual } from 'fast-equals'
import Popover from '../meltComponents/Popover.svelte'
import TextInput from '../text_input/TextInput.svelte'
const DEFAULT_DUCKLAKE_CATALOG_NAME = 'ducklake_catalog'
@@ -229,7 +230,7 @@
<code class="px-1 py-0.5 border rounded-md">ATTACH 'ducklake' AS dl;</code> shorthand
</Tooltip>
{/if}
<input bind:value={ducklake.name} placeholder="Name" />
<TextInput bind:value={ducklake.name} inputProps={{ placeholder: 'Name' }} />
</Cell>
<Cell>
<div class="flex gap-4">
@@ -289,9 +290,12 @@
}
}
class="w-48"
inputClass="!placeholder-primary"
inputClass="!placeholder-secondary"
/>
<TextInput
inputProps={{ placeholder: 'Data path (defaults to /)' }}
bind:value={ducklake.storage.path}
/>
<input placeholder="Data path (defaults to /)" bind:value={ducklake.storage.path} />
</div>
</Cell>
<Cell class="w-12">
@@ -1,7 +1,7 @@
<script lang="ts">
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { emptyString, sendUserToast } from '$lib/utils'
import { ChevronDown, Plus, Shield, X } from 'lucide-svelte'
import { ChevronDown, Plus, Shield } from 'lucide-svelte'
import Alert from '../common/alert/Alert.svelte'
import Button from '../common/button/Button.svelte'
import Tab from '../common/tabs/Tab.svelte'
@@ -19,11 +19,12 @@
import { WorkspaceService } from '$lib/gen'
import S3FilePicker from '../S3FilePicker.svelte'
import Portal from '../Portal.svelte'
import { fade } from 'svelte/transition'
import Popover from '../meltComponents/Popover.svelte'
import ClearableInput from '../common/clearableInput/ClearableInput.svelte'
import MultiSelect from '../select/MultiSelect.svelte'
import CloseButton from '../common/CloseButton.svelte'
import TextInput from '../text_input/TextInput.svelte'
import Select from '../select/Select.svelte'
let { s3ResourceSettings = $bindable() }: { s3ResourceSettings: S3ResourceSettings } = $props()
@@ -114,10 +115,10 @@
<div class="mt-6">
<div class="flex mt-2 flex-col gap-y-4 max-w-5xl">
{#each s3ResourceSettings.secondaryStorage ?? [] as _, idx}
<div class="flex gap-1 items-center">
<input
<div class="flex gap-1 relative">
<TextInput
class="max-w-[200px]"
type="text"
inputProps={{ type: 'text', placeholder: 'Storage name' }}
bind:value={
() => s3ResourceSettings.secondaryStorage?.[idx]?.[0] || '',
(v) => {
@@ -126,9 +127,8 @@
}
}
}
placeholder="Storage name"
/>
<select
<Select
class="max-w-[125px]"
bind:value={
() => s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourceType || 's3',
@@ -138,19 +138,19 @@
}
}
}
>
<option value="s3">S3</option>
<option value="azure_blob">Azure Blob</option>
<option value="s3_aws_oidc">AWS OIDC</option>
<option value="azure_workload_identity">Azure Workload Identity</option>
<option value="gcloud_storage">Google Cloud Storage</option>
</select>
<!-- this can be removed once parent moves to runes -->
<!-- svelte-ignore binding_property_non_reactive -->
items={[
{ value: 's3', label: 'S3' },
{ value: 'azure_blob', label: 'Azure Blob' },
{ value: 's3_aws_oidc', label: 'AWS OIDC' },
{ value: 'azure_workload_identity', label: 'Azure Workload Identity' },
{ value: 'gcloud_storage', label: 'Google Cloud Storage' }
]}
/>
<ResourcePicker
resourceType={s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourceType || 's3'}
bind:value={
() => s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourcePath || '',
() => s3ResourceSettings.secondaryStorage?.[idx]?.[1].resourcePath || undefined,
(v) => {
if (s3ResourceSettings.secondaryStorage?.[idx]) {
s3ResourceSettings.secondaryStorage[idx][1].resourcePath = v
@@ -173,19 +173,16 @@
}
}}>Browse content (save first)</Button
>
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
aria-label="Clear"
onclick={() => {
<CloseButton
class="my-auto"
small
on:close={() => {
if (s3ResourceSettings.secondaryStorage) {
s3ResourceSettings.secondaryStorage.splice(idx, 1)
s3ResourceSettings.secondaryStorage = [...s3ResourceSettings.secondaryStorage]
}
}}
>
<X size={14} />
</button>
/>
</div>
{/each}
<div class="flex gap-1">
@@ -229,7 +226,7 @@
{#snippet permissionBtn(storage: NonNullable<S3ResourceSettings['secondaryStorage']>[number][1])}
<Popover closeOnOtherPopoverOpen placement="left">
<svelte:fragment slot="trigger">
<Button variant="border" btnClasses="px-2.5" color="dark" size="sm">
<Button variant="border" wrapperClasses="h-full" btnClasses="px-2.5" color="dark" size="sm">
<Shield size={16} /> Permissions <ChevronDown size={14} />
</Button>
</svelte:fragment>
@@ -953,6 +953,7 @@
{#if minTs || maxTs}
<input
type="text"
class="!text-sm text-tertiary !bg-surface-secondary h-9 !border-none"
value={minTs ? new Date(minTs).toLocaleString() : 'zoom x axis to set min'}
disabled
name="min-datetimes"
@@ -984,6 +985,7 @@
{#if maxTs || minTs}
<input
type="text"
class="!text-sm text-tertiary !bg-surface-secondary h-9 !border-none"
value={maxTs ? new Date(maxTs).toLocaleString() : 'zoom x axis to set max'}
name="max-datetimes"
disabled
@@ -1013,7 +1015,9 @@
{#if minTs || maxTs}
<RunOption label="Reset" for="reset" noLabel>
<Button color="light" variant="border" size="xs" onClick={reset}>Reset</Button>
<Button color="light" variant="border" size="xs" onClick={reset} btnClasses="h-9">
Reset
</Button>
</RunOption>
{/if}
</div>
+10 -2
View File
@@ -14,6 +14,7 @@ const lightTheme = {
textPrimary: '#2d3748',
textSecondary: '#4a5568',
textTertiary: '#505c70',
textHint: '#757E8F',
textDisabled: '#a0aec0',
border: '#dddddd',
@@ -32,6 +33,7 @@ const darkTheme = {
textPrimary: '#EEEEEE',
textSecondary: '#C2C9D1',
textTertiary: '#A8AEB7',
textHint: '#989DA5',
textDisabled: '#989DA5',
border: '#3e4c60',
@@ -413,6 +415,7 @@ const config = {
primary: 'rgb(var(--color-text-primary) / <alpha-value>)',
secondary: 'rgb(var(--color-text-secondary) / <alpha-value>)',
tertiary: 'rgb(var(--color-text-tertiary) / <alpha-value>)',
hint: 'rgb(var(--color-text-hint) / <alpha-value>)',
disabled: 'rgb(var(--color-text-disabled) / <alpha-value>)',
'surface-inverse': 'rgb(var(--color-surface-inverse) / <alpha-value>)',
@@ -424,6 +427,7 @@ const config = {
'primary-inverse': 'rgb(var(--color-text-primary-inverse) / <alpha-value>)',
'secondary-inverse': 'rgb(var(--color-text-secondary-inverse) / <alpha-value>)',
'tertiary-inverse': 'rgb(var(--color-text-tertiary-inverse) / <alpha-value>)',
'hint-inverse': 'rgb(var(--color-text-hint-inverse) / <alpha-value>)',
'disabled-inverse': 'rgb(var(--color-text-disabled-inverse) / <alpha-value>)'
},
fontFamily: {
@@ -519,6 +523,7 @@ const config = {
'--color-text-primary': lightThemeRgb.textPrimary,
'--color-text-secondary': lightThemeRgb.textSecondary,
'--color-text-tertiary': lightThemeRgb.textTertiary,
'--color-text-hint': lightThemeRgb.textHint,
'--color-text-disabled': lightThemeRgb.textDisabled,
'--color-surface-inverse': darkThemeRgb.surface,
@@ -530,6 +535,7 @@ const config = {
'--color-text-primary-inverse': darkThemeRgb.textPrimary,
'--color-text-secondary-inverse': darkThemeRgb.textSecondary,
'--color-text-tertiary-inverse': darkThemeRgb.textTertiary,
'--color-text-hint-inverse': darkThemeRgb.textHint,
'--color-text-disabled-inverse': darkThemeRgb.textDisabled,
'--color-border': lightThemeRgb.border,
@@ -560,6 +566,7 @@ const config = {
'--color-text-primary': darkThemeRgb.textPrimary,
'--color-text-secondary': darkThemeRgb.textSecondary,
'--color-text-tertiary': darkThemeRgb.textTertiary,
'--color-text-hint': darkThemeRgb.textHint,
'--color-text-disabled': darkThemeRgb.textDisabled,
'--color-surface-inverse': lightThemeRgb.surface,
@@ -571,6 +578,7 @@ const config = {
'--color-text-primary-inverse': lightThemeRgb.textPrimary,
'--color-text-secondary-inverse': lightThemeRgb.textSecondary,
'--color-text-tertiary-inverse': lightThemeRgb.textTertiary,
'--color-text-hint-inverse': lightThemeRgb.textHint,
'--color-text-disabled-inverse': lightThemeRgb.textDisabled,
'--color-border': darkThemeRgb.border,
@@ -661,7 +669,7 @@ const config = {
".dark [type='checkbox']:checked": {
backgroundImage: `url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")`
},
'input:not(.windmillapp),input[type="text"]:not(.windmillapp),input[type="email"]:not(.windmillapp),input[type="url"]:not(.windmillapp),input[type="password"]:not(.windmillapp),input[type="number"]:not(.windmillapp),input[type="date"]:not(.windmillapp),input[type="datetime-local"]:not(.windmillapp),input[type="month"]:not(.windmillapp),input[type="search"]:not(.windmillapp),input[type="tel"]:not(.windmillapp),input[type="time"]:not(.windmillapp),input[type="week"]:not(.windmillapp),textarea:not(.windmillapp):not(.monaco-mouse-cursor-text),select:not(.windmillapp)':
'input:not(.windmillapp):not(.no-default-style),input[type="text"]:not(.windmillapp):not(.no-default-style),input[type="email"]:not(.windmillapp):not(.no-default-style),input[type="url"]:not(.windmillapp):not(.no-default-style),input[type="password"]:not(.windmillapp):not(.no-default-style),input[type="number"]:not(.windmillapp):not(.no-default-style),input[type="date"]:not(.windmillapp):not(.no-default-style),input[type="datetime-local"]:not(.windmillapp):not(.no-default-style),input[type="month"]:not(.windmillapp):not(.no-default-style),input[type="search"]:not(.windmillapp):not(.no-default-style),input[type="tel"]:not(.windmillapp):not(.no-default-style),input[type="time"]:not(.windmillapp):not(.no-default-style),input[type="week"]:not(.windmillapp):not(.no-default-style),textarea:not(.windmillapp):not(.no-default-style):not(.monaco-mouse-cursor-text),select:not(.windmillapp):not(.no-default-style)':
{
display: 'block',
fontSize: theme('fontSize.sm'),
@@ -686,7 +694,7 @@ const config = {
}
}
},
'.dark input:not(.windmillapp),.dark input[type="text"]:not(.windmillapp),.dark input[type="email"]:not(.windmillapp),.dark input[type="url"]:not(.windmillapp),.dark input[type="password"]:not(.windmillapp),.dark input[type="number"]:not(.windmillapp),.dark input[type="date"]:not(.windmillapp),.dark input[type="datetime-local"]:not(.windmillapp),.dark input[type="month"]:not(.windmillapp),.dark input[type="search"]:not(.windmillapp),.dark input[type="tel"]:not(.windmillapp),.dark input[type="time"]:not(.windmillapp),.dark input[type="week"]:not(.windmillapp),.dark textarea:not(.windmillapp):not(.monaco-mouse-cursor-text),.dark select:not(.windmillapp)':
'.dark input:not(.windmillapp):not(.no-default-style),.dark input[type="text"]:not(.windmillapp):not(.no-default-style),.dark input[type="email"]:not(.windmillapp):not(.no-default-style),.dark input[type="url"]:not(.windmillapp):not(.no-default-style),.dark input[type="password"]:not(.windmillapp):not(.no-default-style),.dark input[type="number"]:not(.windmillapp):not(.no-default-style),.dark input[type="date"]:not(.windmillapp):not(.no-default-style),.dark input[type="datetime-local"]:not(.windmillapp):not(.no-default-style),.dark input[type="month"]:not(.windmillapp):not(.no-default-style),.dark input[type="search"]:not(.windmillapp):not(.no-default-style),.dark input[type="tel"]:not(.windmillapp):not(.no-default-style),.dark input[type="time"]:not(.windmillapp):not(.no-default-style),.dark input[type="week"]:not(.windmillapp):not(.no-default-style),.dark textarea:not(.windmillapp):not(.no-default-style):not(.monaco-mouse-cursor-text),.dark select:not(.windmillapp):not(.no-default-style)':
{
backgroundColor: theme('colors.gray.700'),
color: theme('colors.gray.200'),