fix(frontend): improve flow editor settings bar UX (#6049)

* move settings and static inputs into top node

* Move test button in the top nodes

* Revert "Move test button in the top nodes"

This reverts commit 1c8648a538.

* Add error handler to top toolbar

* nit

* polishing

* add flow settings to topbar dropdown

* remove unused files

* progress

* progress

* fixes

* fix

* fix

* fix

* fix

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Guilhem
2025-06-27 16:26:03 +01:00
committed by GitHub
parent bcba46225f
commit ded54f2e68
79 changed files with 2297 additions and 1623 deletions
@@ -4,9 +4,13 @@
import { autoPlacement } from '@floating-ui/core'
import ChangeInstanceUsernameInner from './ChangeInstanceUsernameInner.svelte'
export let email: string
export let username: string
export let isConflict = false
interface Props {
email: string
username: string
isConflict?: boolean
}
let { email, username, isConflict = false }: Props = $props()
</script>
<Popover
@@ -19,12 +23,12 @@
}}
closeButton
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color={isConflict ? 'red' : 'light'} size="xs" spacingSize="xs2" nonCaptureEvent={true}
>{isConflict ? 'Fix username conflict' : 'Change username'}</Button
>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<ChangeInstanceUsernameInner
{email}
{username}
@@ -32,5 +36,5 @@
on:close={() => close()}
on:renamed
/>
</svelte:fragment>
{/snippet}
</Popover>
+10 -5
View File
@@ -3,19 +3,24 @@
import { Button } from './common'
import { Clock } from 'lucide-svelte'
import Popover from './meltComponents/Popover.svelte'
interface Props {
children?: import('svelte').Snippet<[any]>
}
let { children }: Props = $props()
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }} closeButton>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: Clock }}>
Use simplified builder
</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<Section label="CRON Builder" wrapperClass="p-4">
<div class="flex flex-col w-72">
<slot {close} />
{@render children?.({ close })}
</div>
</Section>
</svelte:fragment>
{/snippet}
</Popover>
+209 -193
View File
@@ -10,23 +10,35 @@
import Select from './select/Select.svelte'
import MultiSelect from './select/MultiSelect.svelte'
import { safeSelectItems } from './select/utils.svelte'
import { untrack } from 'svelte'
export let schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
export let timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
export let disabled = false
export let validCRON = true
export let cronVersion: string = 'v2'
interface Props {
schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
disabled?: boolean
validCRON?: boolean
cronVersion?: string
}
let preview: string[] = []
let {
schedule = $bindable(),
timezone = $bindable(),
disabled = false,
validCRON = $bindable(true),
cronVersion = $bindable('v2')
}: Props = $props()
let preview: string[] = $state([])
// If the user has already entered a cron string, switching to the basic tab will override it.
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' = 'minute'
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' =
$state('minute')
let seconds = 30
let minutes = 30
let hours = 1
let seconds = $state(30)
let minutes = $state(30)
let hours = $state(1)
const daysOfMonthOptions: number[] = Array.from(Array(31).keys()).map((i) => i + 1)
let daysOfMonth: number[] = []
let daysOfMonth: number[] = $state([])
// let lastDayOfMonth = false
const monthsOfYearOptions: string[] = [
'January',
@@ -42,7 +54,7 @@
'November',
'December'
]
let monthsOfYear: string[] = []
let monthsOfYear: string[] = $state([])
const daysOfWeekOptions: string[] = [
'Sunday',
'Monday',
@@ -52,10 +64,8 @@
'Friday',
'Saturday'
]
let daysOfWeek: string[] = []
let UTCTime: string = ''
$: !emptyString(schedule) && handleScheduleInput(schedule, timezone)
let daysOfWeek: string[] = $state([])
let UTCTime: string = $state('')
async function handleScheduleInput(input: string, timezone: string): Promise<void> {
try {
@@ -73,9 +83,60 @@
}
}
let nschedule = ''
let nschedule = $state('')
$: {
function formatDate(timezone) {
try {
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
} catch (ee) {
sendUserToast(
`Invalid timezone: ${timezone}. Update your browser's timezone preference`,
true
)
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'Europe/Paris',
timeZoneName: 'short'
}).format
}
}
const items = Object.keys(timezones)
.map((key) => {
return Object.keys(timezones[key])
.map((subKey) => {
return {
value: subKey,
label: subKey,
group: timezones[key][subKey][1] as string
}
})
.flat()
})
.flat()
$effect(() => {
schedule
untrack(() => {
!emptyString(schedule) && handleScheduleInput(schedule, timezone)
})
})
$effect(() => {
// CRON string format
// sec min hour day of month month day of week year
// 0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2
@@ -133,65 +194,18 @@
} else if (executeEvery === 'day-week') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} * * ${s_daysOfWeek}`
}
}
$: dateFormatter = formatDate(timezone)
function formatDate(timezone) {
try {
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
} catch (ee) {
sendUserToast(
`Invalid timezone: ${timezone}. Update your browser's timezone preference`,
true
)
return new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'Europe/Paris',
timeZoneName: 'short'
}).format
}
}
const items = Object.keys(timezones)
.map((key) => {
return Object.keys(timezones[key])
.map((subKey) => {
return {
value: subKey,
label: subKey,
group: timezones[key][subKey][1] as string
}
})
.flat()
})
.flat()
})
let dateFormatter = $derived(formatDate(timezone))
</script>
<div class="w-full flex space-x-8">
<div class="w-full flex flex-col gap-4">
<Label label="Cron" class="font-semibold" primary={true}>
<svelte:fragment slot="error">
{#snippet error()}
{#if !validCRON}
<div class="text-red-600 text-xs"> Invalid cron syntax </div>
{/if}
</svelte:fragment>
{/snippet}
<div class="flex flex-row-reverse text-2xs text-tertiary -mt-1 hover:underline">
<a
class="text-tertiary"
@@ -231,143 +245,145 @@
{#if !disabled}
<div class="flex flex-row gap-2 mb-2">
<CronBuilder let:close>
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Execute schedule every</div>
<CronBuilder>
{#snippet children({ close })}
<div class="w-full flex flex-col">
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Execute schedule every</div>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1 mb-2">
<select
{disabled}
name="execute_every"
id="execute_every"
bind:value={executeEvery}
>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1 mb-2">
<select
{disabled}
name="execute_every"
id="execute_every"
bind:value={executeEvery}
>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input {disabled} type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input {disabled} type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input {disabled} type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
</div>
</div>
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={monthsOfYear}
items={safeSelectItems(monthsOfYearOptions)}
placeholder="Every month"
/>
</div>
{/if}
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfWeek}
items={safeSelectItems(daysOfWeekOptions)}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfMonth}
items={safeSelectItems(daysOfMonthOptions)}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input {disabled} type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input {disabled} type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input {disabled} type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
{/if} -->
</div>
</div>
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={monthsOfYear}
items={safeSelectItems(monthsOfYearOptions)}
placeholder="Every month"
/>
</div>
<small>Schedule will only execute on valid calendar days</small>
{/if}
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfWeek}
items={safeSelectItems(daysOfWeekOptions)}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<MultiSelect
disablePortal
{disabled}
bind:value={daysOfMonth}
items={safeSelectItems(daysOfMonthOptions)}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div>
{/if} -->
</div>
<small>Schedule will only execute on valid calendar days</small>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At Time</small>
<input
{disabled}
type="time"
name="atUTCTime"
id="atUTCTime"
bind:value={UTCTime}
/>
</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Preview New Cron</div>
<div class="flex p-2 px-4 rounded-md bg-surface-secondary">
<span>{nschedule}</span>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At Time</small>
<input
{disabled}
type="time"
name="atUTCTime"
id="atUTCTime"
bind:value={UTCTime}
/>
</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<div class="text-secondary text-sm leading-none">Preview New Cron</div>
<div class="flex p-2 px-4 rounded-md bg-surface-secondary">
<span>{nschedule}</span>
</div>
</div>
</div>
<div class="mt-4">
<Button
color="dark"
size="xs"
on:click={() => {
schedule = nschedule
close()
}}
>
Set cron schedule
</Button>
</div>
<div class="mt-4">
<Button
color="dark"
size="xs"
on:click={() => {
schedule = nschedule
close()
}}
>
Set cron schedule
</Button>
</div>
{/snippet}
</CronBuilder>
<CronGen bind:schedule bind:cronVersion />
</div>
+3 -1
View File
@@ -187,7 +187,9 @@
<div class="flex gap-1 w-full justify-between items-center text-xs text-primary p-2">
<div>
<Popover>
<svelte:fragment slot="text">Download</svelte:fragment>
{#snippet text()}
Download
{/snippet}
<Button
startIcon={{ icon: Download }}
color="light"
+13 -6
View File
@@ -6,14 +6,21 @@
import DefaultTagsInner from './DefaultTagsInner.svelte'
export let defaultTagPerWorkspace: boolean | undefined = undefined
export let defaultTagWorkspaces: string[] = []
interface Props {
defaultTagPerWorkspace?: boolean | undefined
defaultTagWorkspaces?: string[]
}
let {
defaultTagPerWorkspace = $bindable(undefined),
defaultTagWorkspaces = $bindable([])
}: Props = $props()
let placement: 'bottom-end' | 'top-end' = 'bottom-end'
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: placement }} contentClasses="p-4">
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true}>
<div class="flex flex-row gap-1 items-center"
><Pen size={14} /> Default tags&nbsp;<Tooltip light
@@ -22,8 +29,8 @@
></div
>
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<DefaultTagsInner bind:defaultTagPerWorkspace bind:defaultTagWorkspaces />
</svelte:fragment>
{/snippet}
</Popover>
@@ -6,14 +6,27 @@
import type { DisplayResultUi } from './custom_ui'
import { createEventDispatcher } from 'svelte'
export let customUi: DisplayResultUi | undefined = undefined
export let filename: string | undefined = undefined
export let workspaceId: string | undefined = undefined
export let jobId: string | undefined = undefined
export let nodeId: string | undefined = undefined
export let base: string
export let result: any
export let disableTooltips: boolean = false
interface Props {
customUi?: DisplayResultUi | undefined
filename?: string | undefined
workspaceId?: string | undefined
jobId?: string | undefined
nodeId?: string | undefined
base: string
result: any
disableTooltips?: boolean
}
let {
customUi = undefined,
filename = undefined,
workspaceId = undefined,
jobId = undefined,
nodeId = undefined,
base,
result,
disableTooltips = false
}: Props = $props()
const dispatch = createEventDispatcher()
@@ -42,19 +55,19 @@
{/if}
{#if disableTooltips !== true}
<Popover documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering">
<svelte:fragment slot="text">
{#snippet text()}
The result renderer in Windmill supports rich display rendering, allowing you to customize
the display format of your results.
</svelte:fragment>
{/snippet}
<div>
<InfoIcon size={14} />
</div>
</Popover>
{/if}
<button on:click={() => copyToClipboard(toJsonStr(result))}>
<button onclick={() => copyToClipboard(toJsonStr(result))}>
<ClipboardCopy size={14} />
</button>
<button on:click={() => dispatch('open-drawer')}>
<button onclick={() => dispatch('open-drawer')}>
<Expand size={14} />
</button>
</div>
@@ -478,7 +478,7 @@
>{teams_team_name}</Badge
>
</p>
<Tooltip text={teams_team_name}>
<Tooltip>
Each workspace can only be connected to one Microsoft Teams team. You can configure it under <a
target="_blank"
href="{base}/workspace_settings?tab=teams">workspace settings</a
+25 -7
View File
@@ -46,12 +46,20 @@
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
import { cleanInputs } from './flows/utils'
import { Calendar, Pen, Save, DiffIcon, HistoryIcon, FileJson, type Icon } from 'lucide-svelte'
import {
Calendar,
Pen,
Save,
DiffIcon,
HistoryIcon,
FileJson,
type Icon,
Settings
} from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Awareness from './Awareness.svelte'
import { getAllModules } from './flows/flowExplorer'
import { type FlowCopilotContext } from './copilot/flow'
import FlowAIButton from './copilot/chat/flow/FlowAIButton.svelte'
import { loadFlowModuleState } from './flows/flowStateUtils.svelte'
import FlowBuilderTutorials from './FlowBuilderTutorials.svelte'
import Dropdown from '$lib/components/DropdownV2.svelte'
@@ -789,7 +797,7 @@
}
]
: []),
...(customUi?.topBar?.history != false
...(customUi?.topBar?.export != false
? [
{
displayName: 'Export',
@@ -803,6 +811,17 @@
disabled: hasAiDiff
}
]
: []),
...(customUi?.topBar?.settings != false
? [
{
displayName: 'Flow settings',
icon: Settings,
action: () => {
select('settings-metadata')
}
}
]
: [])
]
}
@@ -1068,9 +1087,6 @@
</div>
</Button>
{/if}
{#if !disableAi && customUi?.topBar?.aiBuilder != false && !aiChatManager.open}
<FlowAIButton openPanel={() => aiChatManager.openChat()} />
{/if}
<FlowPreviewButtons
on:openTriggers={(e) => {
select('triggers')
@@ -1103,7 +1119,6 @@
/>
</div>
</div>
<!-- metadata -->
{#if $flowStateStore}
<FlowEditor
@@ -1143,6 +1158,9 @@
}}
{forceTestTab}
{highlightArg}
aiChatOpen={aiChatManager.open}
showFlowAiButton={!disableAi && customUi?.topBar?.aiBuilder != false}
toggleAiChat={() => aiChatManager.toggleOpen()}
onRunPreview={() => {
flowPreviewButtons?.openPreview(true)
}}
+127 -118
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import {
type Folder,
@@ -21,18 +23,22 @@
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 = 'viewer' | 'writer' | 'admin'
let folder: Folder | undefined
let perms: { owner_name: string; role: Role }[] | undefined = undefined
let usernames: string[] = []
let groups: string[] = []
let ownerItem: string = ''
let perms: { owner_name: string; role: Role }[] | undefined = $state(undefined)
let usernames: string[] = $state([])
let groups: string[] = $state([])
let ownerItem: string = $state('')
let newGroup: Drawer
let viewGroup: Drawer
let newGroup: Drawer | undefined = $state(undefined)
let viewGroup: Drawer | undefined = $state(undefined)
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
@@ -42,12 +48,6 @@
groups = await GroupService.listGroupNames({ workspace: $workspaceStore! })
}
$: {
if ($workspaceStore && $userStore) {
load()
}
}
async function load() {
loadUsernames()
loadGroups()
@@ -67,7 +67,7 @@
loadFolder()
}
let folderNotFound: boolean | undefined = undefined
let folderNotFound: boolean | undefined = $state(undefined)
async function loadFolder(): Promise<void> {
try {
@@ -111,10 +111,10 @@
}
}
let ownerKind: 'user' | 'group' = 'user'
let groupCreated: string | undefined = undefined
let newGroupName: string = ''
let summary: string = ''
let ownerKind: 'user' | 'group' = $state('user')
let groupCreated: string | undefined = $state(undefined)
let newGroupName: string = $state('')
let summary: string = $state('')
async function addGroup() {
await GroupService.createGroup({
@@ -139,13 +139,18 @@
dispatch('update')
loadFolder()
}
run(() => {
if ($workspaceStore && $userStore) {
load()
}
})
</script>
<Drawer bind:this={newGroup}>
<DrawerContent
title="New Group"
on:close={() => {
newGroup.closeDrawer()
newGroup?.closeDrawer()
groupCreated = undefined
}}
>
@@ -180,7 +185,7 @@
<Section label={`Permissions (${perms?.length ?? 0})`}>
<div class="flex flex-col gap-6">
{#if can_write}
<Alert role="info" title="New permissions may take up to 60s to apply">
<Alert type="info" title="New permissions may take up to 60s to apply">
<span class="text-xs text-tertiary">Due to permissions cache invalidation </span>
</Alert>
<div class="flex items-center gap-1">
@@ -260,116 +265,120 @@
{/if}
{#if perms}
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user/group</th>
<th></th>
<th></th>
</tr>
<tbody slot="body">
{#each perms as { owner_name, role }}<tr>
<td>{owner_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username && !$userStore?.is_admin}
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
// const inAcl = (
// folder?.extra_perms ? Object.keys(folder?.extra_perms) : []
// ).includes(folder)
if (role == 'admin') {
await FolderService.addOwnerToFolder({
{#snippet body()}
<tbody>
{#each perms ?? [] as { owner_name, role }}<tr>
<td>{owner_name}</td>
<td>
{#if can_write}
<div>
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username &&
!$userStore?.is_admin}
selected={role}
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
// const inAcl = (
// folder?.extra_perms ? Object.keys(folder?.extra_perms) : []
// ).includes(folder)
if (role == 'admin') {
await FolderService.addOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name
}
})
} else if (role == 'writer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: true
}
})
} else if (role == 'viewer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: false
}
})
}
loadFolder()
}}
>
{#snippet children({ item })}
<ToggleButton
value="viewer"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<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"
{item}
/>
<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"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write && (owner_name != 'u/' + $userStore?.username || $userStore?.is_admin)}
<button
class="ml-2 text-red-500"
onclick={async () => {
await Promise.all([
FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: { owner: owner_name }
}),
GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
} else if (role == 'writer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: true
}
})
} else if (role == 'viewer') {
await FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: {
owner: owner_name,
write: false
}
})
}
])
loadFolder()
}}
}}>remove</button
>
{#snippet children({ item })}
<ToggleButton
value="viewer"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<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"
{item}
/>
<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"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
{role}
{/if}</td
>
<td>
{#if can_write && (owner_name != 'u/' + $userStore?.username || $userStore?.is_admin)}
<button
class="ml-2 text-red-500"
on:click={async () => {
await Promise.all([
FolderService.removeOwnerToFolder({
workspace: $workspaceStore ?? '',
name,
requestBody: { owner: owner_name }
}),
GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'folder',
requestBody: {
owner: owner_name
}
})
])
loadFolder()
}}>remove</button
>
{:else}
<span class="text-tertiary text-xs">cannot remove yourself</span>
{/if}</td
>
</tr>{/each}
</tbody>
{:else}
<span class="text-tertiary text-xs">cannot remove yourself</span>
{/if}</td
>
</tr>{/each}
</tbody>
{/snippet}
</TableCustom>
<!-- <h2 class="mt-10"
>Folders managing this folder <Tooltip
@@ -1,7 +1,11 @@
<script lang="ts">
import Popover from './Popover.svelte'
export let members: string[]
interface Props {
members: string[]
}
let { members }: Props = $props()
</script>
<Popover>
@@ -11,5 +15,7 @@
<span class="text-tertiary text-xs">{members?.join(', ')} </span>
</div>
</div>
<span slot="text">{members?.join(', ')} </span>
{#snippet text()}
<span>{members?.join(', ')} </span>
{/snippet}
</Popover>
+12 -4
View File
@@ -2,16 +2,22 @@
import { GroupService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Popover from './Popover.svelte'
import { untrack } from 'svelte'
export let name: string
interface Props {
name: string
}
$: $workspaceStore && loadMembers()
let { name }: Props = $props()
let members: string[] | undefined = []
let members: string[] | undefined = $state([])
async function loadMembers() {
members = (await GroupService.getGroup({ workspace: $workspaceStore!, name })).members
}
$effect(() => {
$workspaceStore && untrack(() => loadMembers())
})
</script>
{#if members}
@@ -22,6 +28,8 @@
><span class="text-tertiary text-xs">{members?.join(', ')}</span></div
></div
>
<span slot="text">{members?.join(', ')}</span></Popover
{#snippet text()}
<span>{members?.join(', ')}</span>
{/snippet}</Popover
>
{/if}
+10 -15
View File
@@ -1,7 +1,6 @@
<script lang="ts" module>
type testModuleState = {
loading: boolean
instances: number
cancel?: () => void
}
@@ -111,14 +110,7 @@
const modId = mod.id
testModulesState[modId] = {
...(testModulesState[modId] ?? { loading: false, instances: 0 }),
loading: testIsLoading,
instances: testModulesState[modId]!.instances + 1
}
return () => {
testModulesState[modId].instances -= 1
if (testModulesState[modId].instances < 1) {
delete testModulesState[modId]
}
loading: testIsLoading
}
})
</script>
@@ -130,12 +122,15 @@
bind:this={testJobLoader}
bind:isLoading={
() => testModulesState[mod.id]?.loading ?? false,
(v) =>
(testModulesState[mod.id] = {
...testModulesState[mod.id],
loading: v ?? false,
instances: testModulesState[mod.id]?.instances ?? 0
})
(v) => {
let newLoading = v ?? false
if (testModulesState[mod.id]?.loading !== newLoading) {
testModulesState[mod.id] = {
...(testModulesState[mod.id] ?? {}),
loading: newLoading
}
}
}
}
bind:job={testJob}
/>
@@ -4,6 +4,8 @@
</script>
<Popover notClickable>
<svelte:fragment slot="text">The script has no main function exported</svelte:fragment>
{#snippet text()}
The script has no main function exported
{/snippet}
<Badge small color="yellow" baseClass="border border-indigo-200">No main</Badge>
</Popover>
+49 -23
View File
@@ -5,16 +5,39 @@
import { twMerge } from 'tailwind-merge'
import { ExternalLink } from 'lucide-svelte'
import { untrack } from 'svelte'
export let placement: PopoverPlacement = 'bottom-end'
export let notClickable = false
export let popupClass = ''
export let disablePopup = false
export let disappearTimeout = 100
export let appearTimeout = 300
export let documentationLink: string | undefined = undefined
export let style: string | undefined = undefined
export let forceOpen = false
interface Props {
placement?: PopoverPlacement
notClickable?: boolean
popupClass?: string
disablePopup?: boolean
disappearTimeout?: number
appearTimeout?: number
documentationLink?: string | undefined
style?: string | undefined
forceOpen?: boolean
class?: string
children?: import('svelte').Snippet
text?: import('svelte').Snippet
onClick?: () => void
}
let {
placement = 'bottom-end',
notClickable = false,
popupClass = '',
disablePopup = false,
disappearTimeout = 100,
appearTimeout = 300,
documentationLink = undefined,
style = undefined,
forceOpen = false,
class: classNames = '',
children,
text,
onClick
}: Props = $props()
const [popperRef, popperContent] = createPopperActions({ placement })
@@ -32,7 +55,7 @@
]
}
let showTooltip = false
let showTooltip = $state(false)
let timeout: NodeJS.Timeout | undefined = undefined
let inTimeout: NodeJS.Timeout | undefined = undefined
@@ -50,40 +73,43 @@
timeout = setTimeout(() => (showTooltip = false), disappearTimeout)
}
$: forceOpen ? open() : close()
$effect(() => {
;[forceOpen]
untrack(() => (forceOpen ? open() : close()))
})
</script>
{#if notClickable}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span {style} use:popperRef on:mouseenter={open} on:mouseleave={close} class={$$props.class}>
<slot />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span {style} use:popperRef onmouseenter={open} onmouseleave={close} class={classNames}>
{@render children?.()}
</span>
{:else}
<button
{style}
use:popperRef
on:mouseenter={open}
on:mouseleave={close}
on:click
class={$$props.class}
onmouseenter={open}
onmouseleave={close}
onclick={onClick}
class={classNames}
>
<slot />
{@render children?.()}
</button>
{/if}
{#if showTooltip && !disablePopup}
<Portal name="popover">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
use:popperContent={popperOptions}
on:mouseenter={open}
on:mouseleave={close}
onmouseenter={open}
onmouseleave={close}
class={twMerge(
'z-[5001] py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800 whitespace-normal text-left',
popupClass
)}
>
<div class="max-w-sm break-words">
<slot name="text" />
{@render text?.()}
{#if documentationLink}
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
<div class="flex flex-row gap-2 mt-4">
@@ -1,21 +1,31 @@
<script lang="ts">
import { JobService, type Preview } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy, tick } from 'svelte'
import { onDestroy, tick, untrack } from 'svelte'
import { createEventDispatcher } from 'svelte'
import type { SupportedLanguage } from '$lib/common'
export let isLoading = false
export let job: { completed: boolean; result: any; id: string; success?: boolean } | undefined =
undefined
export let workspaceOverride: string | undefined = undefined
export let notfound = false
export let isEditor = false
export let allowConcurentRequests = false
interface Props {
isLoading?: boolean
job?: { completed: boolean; result: any; id: string; success?: boolean } | undefined
workspaceOverride?: string | undefined
notfound?: boolean
isEditor?: boolean
allowConcurentRequests?: boolean
}
let {
isLoading = $bindable(false),
job = $bindable(undefined),
workspaceOverride = undefined,
notfound = $bindable(false),
isEditor = false,
allowConcurentRequests = false
}: Props = $props()
const dispatch = createEventDispatcher()
$: workspace = workspaceOverride ?? $workspaceStore!
let workspace = $derived(workspaceOverride ?? $workspaceStore!)
let syncIteration: number = 0
let errorIteration = 0
@@ -24,9 +34,16 @@
let ITERATIONS_BEFORE_SUPER_SLOW_REFRESH = 100
let lastStartedAt: number = Date.now()
let currentId: string | undefined = undefined
let currentId: string | undefined = $state(undefined)
$: isLoading = currentId !== undefined
$effect(() => {
let newIsLoading = currentId !== undefined
untrack(() => {
if (isLoading !== newIsLoading) {
isLoading = newIsLoading
}
})
})
type Callbacks = { done: (x: any) => void; cancel: () => void; error: (err: Error) => void }
+33 -21
View File
@@ -4,16 +4,22 @@
import Badge from './common/badge/Badge.svelte'
import Popover from './Popover.svelte'
export let extraPerms: Record<string, boolean> = {}
export let canWrite: boolean
interface Props {
extraPerms?: Record<string, boolean>
canWrite: boolean
}
let kind: 'read' | 'write' | undefined = undefined
let reason = ''
let { extraPerms = {}, canWrite }: Props = $props()
$: {
let { reason, kind } = $derived.by(() => {
if ($userStore?.is_admin || $userStore?.is_super_admin) {
kind = undefined
return {
reason: '',
kind: undefined
}
} else {
let kd: 'read' | 'write' | undefined = undefined
let rson = ''
let username = $userStore?.username ?? ''
let pgroups = $userStore?.pgroups ?? []
let pusername = `u/${username}`
@@ -21,42 +27,48 @@
if (pusername in extraPermsKeys) {
if (extraPerms?.[pusername]) {
kind = 'write'
kd = 'write'
} else {
kind = 'read'
kd = 'read'
}
reason = 'This item was shared to you personally'
rson = 'This item was shared to you personally'
} else {
let writeGroup = pgroups.find((x) => extraPermsKeys.includes(x) && extraPerms?.[x])
if (writeGroup) {
kind = 'write'
reason = `This item was write shared to the group ${writeGroup} which you are a member of`
kd = 'write'
rson = `This item was write shared to the group ${writeGroup} which you are a member of`
} else {
let readGroup = pgroups.find((x) => extraPermsKeys.includes(x))
if (readGroup) {
kind = 'read'
reason = `This item was read-only shared to the group ${readGroup} which you are a member of`
kd = 'read'
rson = `This item was read-only shared to the group ${readGroup} which you are a member of`
} else {
kind = undefined
kd = undefined
}
}
}
if (kind == 'read' && canWrite) {
kind = undefined
if (kd == 'read' && canWrite) {
kd = undefined
}
if (kind == undefined && !canWrite) {
kind = 'read'
reason = ''
if (kd == undefined && !canWrite) {
kd = 'read'
rson = ''
}
return {
reason: rson,
kind: kd
}
}
}
})
</script>
{#if kind === 'read' || kind === 'write'}
<Badge capitalize color="blue" baseClass="border border-blue-200 flex gap-1 items-center">
<Popover notClickable>
<Users size={12} />
<span slot="text">{kind == 'read' ? 'Read & Run only' : 'Read & Write'} {reason}</span>
{#snippet text()}
<span>{kind == 'read' ? 'Read & Run only' : 'Read & Write'} {reason}</span>
{/snippet}
</Popover>
</Badge>
{/if}
@@ -1,22 +1,38 @@
<script lang="ts">
import { type Job, JobService, type FlowStatus, type Preview } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy, tick } from 'svelte'
import { onDestroy, tick, untrack } from 'svelte'
import { createEventDispatcher } from 'svelte'
import type { SupportedLanguage } from '$lib/common'
import { sendUserToast } from '$lib/toast'
import { isScriptPreview } from '$lib/utils'
export let isLoading = false
export let job: Job | undefined = undefined
export let workspaceOverride: string | undefined = undefined
export let notfound = false
export let jobUpdateLastFetch: Date | undefined = undefined
export let toastError = false
export let lazyLogs = false
// Will be set to number if job is not a flow
// If you want to find out progress of subjobs of a flow, check job.flow_status.progress
export let scriptProgress: number | undefined = undefined
interface Props {
isLoading?: boolean
job?: Job | undefined
workspaceOverride?: string | undefined
notfound?: boolean
jobUpdateLastFetch?: Date | undefined
toastError?: boolean
lazyLogs?: boolean
// If you want to find out progress of subjobs of a flow, check job.flow_status.progress
scriptProgress?: number | undefined
children?: import('svelte').Snippet<[any]>
}
let {
isLoading = $bindable(false),
job = $bindable(undefined),
workspaceOverride = undefined,
notfound = $bindable(false),
jobUpdateLastFetch = $bindable(undefined),
toastError = false,
lazyLogs = false,
scriptProgress = $bindable(undefined),
children
}: Props = $props()
/// Last time asked for job progress
let lastTimeCheckedProgress: number | undefined = undefined
@@ -29,7 +45,7 @@
const dispatch = createEventDispatcher()
$: workspace = workspaceOverride ?? $workspaceStore
let workspace = $derived(workspaceOverride ?? $workspaceStore)
let syncIteration: number = 0
let errorIteration = 0
@@ -40,9 +56,16 @@
let ITERATIONS_BEFORE_SUPER_SLOW_REFRESH = 100
let lastStartedAt: number = Date.now()
let currentId: string | undefined = undefined
let currentId: string | undefined = $state(undefined)
$: isLoading = currentId !== undefined
$effect(() => {
let newIsLoading = currentId !== undefined
untrack(() => {
if (isLoading !== newIsLoading) {
isLoading = newIsLoading
}
})
})
export async function abstractRun(fn: () => Promise<string>) {
try {
@@ -306,18 +329,18 @@
})
</script>
<slot
{job}
{isLoading}
{workspaceOverride}
{notfound}
{abstractRun}
{runScriptByPath}
{runFlowByPath}
{runPreview}
{cancelJob}
{clearCurrentJob}
{watchJob}
{loadTestJob}
{syncer}
/>
{@render children?.({
job,
isLoading,
workspaceOverride,
notfound,
abstractRun,
runScriptByPath,
runFlowByPath,
runPreview,
cancelJob,
clearCurrentJob,
watchJob,
loadTestJob,
syncer
})}
+31 -17
View File
@@ -4,15 +4,29 @@
import { ExternalLink } from 'lucide-svelte'
import Popover from './Popover.svelte'
export let position: 'center' | 'left' | 'right' = 'center'
export let total: number
export let min: number | undefined
export let started_at: number | undefined
export let len: number
export let id: string
export let running: boolean
export let concat: boolean = false
export let gray: boolean = false
interface Props {
position?: 'center' | 'left' | 'right'
total: number
min: number | undefined
started_at: number | undefined
len: number
id: string
running: boolean
concat?: boolean
gray?: boolean
}
let {
position = 'center',
total,
min,
started_at,
len,
id,
running,
concat = false,
gray = false
}: Props = $props()
</script>
{#if min && started_at != undefined}
@@ -24,18 +38,18 @@
class="h-4 {gray
? 'bg-gray-300 dark:bg-gray-600'
: running
? 'bg-blue-400/90'
: 'bg-blue-500/90'} {position == 'left'
? 'bg-blue-400/90'
: 'bg-blue-500/90'} {position == 'left'
? 'rounded-l-sm'
: position == 'right'
? 'rounded-r-sm'
: 'rounded-sm'} center-center text-white text-2xs whitespace-nowrap hover:outline outline-1 outline-black"
? 'rounded-r-sm'
: 'rounded-sm'} center-center text-white text-2xs whitespace-nowrap hover:outline outline-1 outline-black"
>
<svelte:fragment slot="text"
><a href="{base}/run/{id}" class="inline-flex items-center gap-1" target="_blank"
{#snippet text()}
<a href="{base}/run/{id}" class="inline-flex items-center gap-1" target="_blank"
>{id} <ExternalLink size={14} /></a
></svelte:fragment
>
>
{/snippet}
{#if len > 0}
<span class={len / total < 0.09 ? '-ml-14 text-primary font-mono' : 'font-mono'}
>{#if len}{msToSec(len, 1)}s{/if}</span
+27 -11
View File
@@ -5,13 +5,29 @@
import { ExternalLink, InfoIcon } from 'lucide-svelte'
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
import { getContext, hasContext } from 'svelte'
export let light = false
export let wrapperClass = ''
export let placement: PopoverPlacement | undefined = undefined
export let documentationLink: string | undefined = undefined
export let small = false
export let markdownTooltip: string | undefined = undefined
export let customSize: string = '100%'
interface Props {
light?: boolean
wrapperClass?: string
placement?: PopoverPlacement | undefined
documentationLink?: string | undefined
small?: boolean
markdownTooltip?: string | undefined
customSize?: string
class?: string
children?: import('svelte').Snippet
}
let {
light = false,
wrapperClass = '',
placement = undefined,
documentationLink = undefined,
small = false,
markdownTooltip = undefined,
customSize = '100%',
class: classNames = '',
children
}: Props = $props()
const plugins = [gfmPlugin()]
const disableTooltips = hasContext('disableTooltips')
@@ -29,17 +45,17 @@
<div
class="inline-flex w-3 mx-0.5 h-3 {light
? 'text-tertiary-inverse'
: 'text-tertiary'} {$$props.class} relative"
: 'text-tertiary'} {classNames} relative"
>
<InfoIcon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 14} />
</div>
<svelte:fragment slot="text">
{#snippet text()}
{#if markdownTooltip}
<div class="prose-sm">
<Markdown md={markdownTooltip} {plugins} />
</div>
{:else}
<slot />
{@render children?.()}
{/if}
{#if documentationLink}
@@ -50,6 +66,6 @@
</div>
</a>
{/if}
</svelte:fragment>
{/snippet}
</Popover>
{/if}
@@ -1,4 +1,7 @@
<script lang="ts">
import { createBubbler, preventDefault } from 'svelte/legacy'
const bubble = createBubbler()
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
@@ -11,26 +14,31 @@
import { twMerge } from 'tailwind-merge'
import AppNavbarItem from './AppNavbarItem.svelte'
export let id: string
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'navbarcomponent'> | undefined = undefined
export let render: boolean
export let navbarItems: NavbarItem[] = []
interface Props {
id: string
configuration: RichConfigurations
customCss?: ComponentCustomCSS<'navbarcomponent'> | undefined
render: boolean
navbarItems?: NavbarItem[]
}
let { id, configuration, customCss = undefined, render, navbarItems = [] }: Props = $props()
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
let resolvedConfig = initConfig(
components['navbarcomponent'].initialData.configuration,
configuration
let resolvedConfig = $state(
initConfig(components['navbarcomponent'].initialData.configuration, configuration)
)
let output = initOutput($worldStore, id, {
result: {
currentPath: undefined as string | undefined
}
})
let output = $state(
initOutput($worldStore, id, {
result: {
currentPath: undefined as string | undefined
}
})
)
let css = initCss($app.css?.navbarcomponent, customCss)
let css = $state(initCss($app.css?.navbarcomponent, customCss))
</script>
{#each Object.keys(components['navbarcomponent'].initialData.configuration) as key (key)}
@@ -63,14 +71,14 @@
>
{#if resolvedConfig.logo?.selected === 'yes'}
<img
on:pointerdown|preventDefault
onpointerdown={preventDefault(bubble('pointerdown'))}
src={resolvedConfig.logo?.configuration?.yes?.sourceKind == 'png encoded as base64'
? 'data:image/png;base64,' + resolvedConfig.logo?.configuration?.yes?.source
: resolvedConfig.logo?.configuration?.yes?.sourceKind == 'jpeg encoded as base64'
? 'data:image/jpeg;base64,' + resolvedConfig.logo?.configuration?.yes?.source
: resolvedConfig.logo?.configuration?.yes?.sourceKind == 'svg encoded as base64'
? 'data:image/svg+xml;base64,' + resolvedConfig.logo?.configuration?.yes?.source
: resolvedConfig.logo?.configuration?.yes?.source}
? 'data:image/jpeg;base64,' + resolvedConfig.logo?.configuration?.yes?.source
: resolvedConfig.logo?.configuration?.yes?.sourceKind == 'svg encoded as base64'
? 'data:image/svg+xml;base64,' + resolvedConfig.logo?.configuration?.yes?.source
: resolvedConfig.logo?.configuration?.yes?.source}
alt={resolvedConfig.logo?.configuration?.yes?.altText}
style={css?.image?.style ?? ''}
class={twMerge(`w-auto h-8`, css?.image?.class, 'wm-image')}
@@ -88,7 +96,9 @@
>
{#each navbarItems ?? [] as navbarItem, index (index)}
<Popover notClickable disablePopup={!Boolean(navbarItem.caption)}>
<svelte:fragment slot="text">{navbarItem.caption}</svelte:fragment>
{#snippet text()}
{navbarItem.caption}
{/snippet}
<AppNavbarItem
{navbarItem}
{id}
@@ -18,7 +18,7 @@
type DbType,
getTablesByResource
} from './utils'
import { getContext, tick } from 'svelte'
import { getContext, tick, untrack } from 'svelte'
import UpdateCell from './UpdateCell.svelte'
import { workspaceStore, type DBSchemas } from '$lib/stores'
import { Drawer } from '$lib/components/common'
@@ -39,18 +39,29 @@
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
import InsertRowDrawerButton from '../InsertRowDrawerButton.svelte'
export let id: string
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'dbexplorercomponent'> | undefined = undefined
export let render: boolean
export let initializing: boolean = true
export let actions: TableAction[] = []
interface Props {
id: string
configuration: RichConfigurations
customCss?: ComponentCustomCSS<'dbexplorercomponent'> | undefined
render: boolean
initializing?: boolean
actions?: TableAction[]
}
$: table = resolvedConfig.type.configuration?.[resolvedConfig.type?.selected]?.table as
| string
| undefined
let {
id,
configuration,
customCss = undefined,
render,
initializing = $bindable(undefined),
actions = []
}: Props = $props()
$: table !== null && render && clearColumns()
$effect.pre(() => {
if (initializing === undefined) {
initializing = true
}
})
function clearColumns() {
// We only want to clear the columns if the table has changed
@@ -78,19 +89,10 @@
$app = $app
}
const resolvedConfig = initConfig(
components['dbexplorercomponent'].initialData.configuration,
configuration
const resolvedConfig = $state(
initConfig(components['dbexplorercomponent'].initialData.configuration, configuration)
)
$: resolvedConfig.type.selected &&
render &&
computeInput(
resolvedConfig.columnDefs,
resolvedConfig.whereClause,
resolvedConfig.type.configuration[resolvedConfig.type.selected].resource
)
let timeoutInput: NodeJS.Timeout | undefined = undefined
function computeInput(columnDefs: any, whereClause: string | undefined, resource: any) {
@@ -115,34 +117,20 @@
getContext<AppViewerContext>('AppViewerContext')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
let input: AppInput | undefined = undefined
let quicksearch = ''
let aggrid: AppAggridExplorerTable
let input: AppInput | undefined = $state(undefined)
let quicksearch = $state('')
let aggrid: AppAggridExplorerTable | undefined = $state()
$: editorContext != undefined && $mode == 'dnd' && resolvedConfig.type && listTables()
$: editorContext != undefined &&
$mode == 'dnd' &&
resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table &&
listColumnsIfAvailable()
let firstQuicksearch = true
$: if (quicksearch !== undefined) {
if (firstQuicksearch) {
firstQuicksearch = false
} else {
aggrid?.clearRows()
}
}
let firstQuicksearch = $state(true)
initializing = false
let updateCell: UpdateCell
let updateCell: UpdateCell | undefined = $state()
let renderCount = 0
let renderCount = $state(0)
let insertDrawer: Drawer | undefined = undefined
let componentContainerHeight: number | undefined = undefined
let buttonContainerHeight: number | undefined = undefined
let componentContainerHeight: number | undefined = $state(undefined)
let buttonContainerHeight: number | undefined = $state(undefined)
function onUpdate(
e: CustomEvent<{
@@ -265,7 +253,7 @@
} catch (e) {}
}
let datasource: IDatasource = {
let datasource: IDatasource = $state({
rowCount: 0,
getRows: async function (params) {
const currentParams = {
@@ -326,9 +314,9 @@
}
})
}
}
})
let lastTable: string | undefined = undefined
let lastTable: string | undefined = $state(undefined)
let timeout: NodeJS.Timeout | undefined = undefined
function isSubset(subset: Record<string, any>, superset: Record<string, any>) {
@@ -442,7 +430,7 @@
return o
})
state = undefined
componentState = undefined
// If in the mean time the table has changed, we don't want to update the columnDefs
if (lastTable !== table) {
@@ -468,8 +456,6 @@
}, 1500)
}
$: $worldStore && render && connectToComponents()
function connectToComponents() {
if ($worldStore && datasource !== undefined) {
const outputs = $worldStore.outputsById[`${id}_count`]
@@ -528,11 +514,11 @@
}
}
let runnableComponent: RunnableComponent
let state: any = undefined
let insertRowRunnable: InsertRowRunnable
let deleteRow: DeleteRow
let dbExplorerCount: DbExplorerCount | undefined = undefined
let runnableComponent: RunnableComponent | undefined = $state()
let componentState: any = $state(undefined)
let insertRowRunnable: InsertRowRunnable | undefined = $state()
let deleteRow: DeleteRow | undefined = $state()
let dbExplorerCount: DbExplorerCount | undefined = $state(undefined)
function onDelete(e) {
const data = { ...e.detail }
@@ -549,12 +535,54 @@
)
}
let refreshCount = 0
let refreshCount = $state(0)
$: hideSearch = resolvedConfig.hideSearch as boolean
$: hideInsert = resolvedConfig.hideInsert as boolean
let loading: boolean = false
let loading: boolean = $state(false)
let table = $derived(
resolvedConfig.type.configuration?.[resolvedConfig.type?.selected]?.table as string | undefined
)
$effect(() => {
table !== null && render && untrack(() => clearColumns())
})
$effect(() => {
;[resolvedConfig.columnDefs, resolvedConfig.whereClause, resolvedConfig.type.selected]
resolvedConfig.type.selected &&
render &&
untrack(() => {
computeInput(
resolvedConfig.columnDefs,
resolvedConfig.whereClause,
resolvedConfig.type.configuration[resolvedConfig.type.selected].resource
)
})
})
$effect(() => {
editorContext != undefined &&
$mode == 'dnd' &&
resolvedConfig.type &&
resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.resource &&
untrack(() => listTables())
})
$effect(() => {
editorContext != undefined &&
$mode == 'dnd' &&
resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table &&
untrack(() => listColumnsIfAvailable())
})
$effect(() => {
if (quicksearch !== undefined) {
if (firstQuicksearch) {
firstQuicksearch = false
} else if (aggrid) {
untrack(() => aggrid?.clearRows())
}
}
})
$effect(() => {
$worldStore && render && untrack(() => connectToComponents())
})
let hideSearch = $derived(resolvedConfig.hideSearch as boolean)
let hideInsert = $derived(resolvedConfig.hideInsert as boolean)
</script>
{#each Object.keys(components['dbexplorercomponent'].initialData.configuration) as key (key)}
@@ -652,7 +680,7 @@
<!-- {JSON.stringify(resolvedConfig.columnDefs)} -->
<AppAggridExplorerTable
bind:this={aggrid}
bind:state
bind:componentState
{id}
{datasource}
{resolvedConfig}
@@ -1,7 +1,9 @@
<script lang="ts">
import { stopPropagation } from 'svelte/legacy'
import { GridApi, createGrid, type IDatasource } from 'ag-grid-community'
import { sendUserToast } from '$lib/utils'
import { createEventDispatcher, getContext, mount, unmount } from 'svelte'
import { createEventDispatcher, getContext, mount, unmount, untrack } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS, ContextPanelContext } from '../../../types'
import type { TableAction, components } from '$lib/components/apps/editor/component'
@@ -22,28 +24,44 @@
import Popover from '$lib/components/Popover.svelte'
import { stateSnapshot, withProps } from '$lib/svelte5Utils.svelte'
export let id: string
export let customCss: ComponentCustomCSS<'aggridcomponent'> | undefined = undefined
export let containerHeight: number | undefined = undefined
export let resolvedConfig: InitConfig<
| (typeof components)['dbexplorercomponent']['initialData']['configuration']
| (typeof components)['aggridinfinitecomponent']['initialData']['configuration']
| (typeof components)['aggridinfinitecomponentee']['initialData']['configuration']
>
export let datasource: IDatasource
export let state: any = undefined
export let outputs: Record<string, Output<any>>
export let allowDelete: boolean
export let actions: TableAction[] = []
export let result: any[] | undefined = undefined
export let allowColumnDefsActions: boolean = true
interface Props {
id: string
customCss?: ComponentCustomCSS<'aggridcomponent'> | undefined
containerHeight?: number | undefined
resolvedConfig: InitConfig<
| (typeof components)['dbexplorercomponent']['initialData']['configuration']
| (typeof components)['aggridinfinitecomponent']['initialData']['configuration']
| (typeof components)['aggridinfinitecomponentee']['initialData']['configuration']
>
datasource: IDatasource
componentState?: any
outputs: Record<string, Output<any>>
allowDelete: boolean
actions?: TableAction[]
result?: any[] | undefined
allowColumnDefsActions?: boolean
}
let {
id,
customCss = undefined,
containerHeight = undefined,
resolvedConfig,
datasource,
componentState = $bindable(undefined),
outputs,
allowDelete,
actions = [],
result = undefined,
allowColumnDefsActions = true
}: Props = $props()
let inputs = {}
const context = getContext<AppViewerContext>('AppViewerContext')
const contextPanel = getContext<ContextPanelContext>('ContextPanel')
const { app, selectedComponent, componentControl, darkMode, mode } = context
let css = initCss($app.css?.aggridcomponent, customCss)
let css = $state(initCss($app.css?.aggridcomponent, customCss))
let selectedRowIndex = -1
@@ -76,8 +94,8 @@
)
}
let clientHeight
let clientWidth
let clientHeight = $state()
let clientWidth = $state()
const dispatch = createEventDispatcher()
@@ -104,10 +122,8 @@
resolvedConfig?.extraConfig?.['defaultColDef']?.['onCellValueChanged']?.(event)
}
let api: GridApi<any> | undefined = undefined
let eGui: HTMLDivElement
$: eGui && mountGrid()
let api: GridApi<any> | undefined = $state(undefined)
let eGui: HTMLDivElement | undefined = $state()
function refreshActions(actions: TableAction[]) {
if (!deepEqual(actions, lastActions)) {
@@ -117,7 +133,6 @@
}
let lastActions: TableAction[] | undefined = undefined
$: actions && refreshActions(actions)
const tableActionsFactory = cellRendererFactory((c, p) => {
const rowIndex = p.node.rowIndex ?? 0
@@ -255,8 +270,8 @@
})
}
let firstRow: number = 0
let lastRow: number = 0
let firstRow: number = $state(0)
let lastRow: number = $state(0)
function validateColumnDefs(columnDefs: ColumnDef[]): {
isValid: boolean
@@ -325,7 +340,7 @@
rowMultiSelectWithClick: resolvedConfig?.multipleSelectable
? resolvedConfig.rowMultiselectWithClick
: false,
initialState: state,
initialState: componentState,
suppressRowDeselection: true,
enableCellTextSelection: true,
...(resolvedConfig?.extraConfig ?? {}),
@@ -340,7 +355,7 @@
lastRow = e.lastRow
},
onStateUpdated: (e) => {
state = e?.api?.getState()
componentState = e?.api?.getState()
resolvedConfig?.extraConfig?.['onStateUpdated']?.(e)
},
onGridReady: (e) => {
@@ -371,22 +386,9 @@
}
}
$: api && resolvedConfig && updateOptions()
let oldDatasource = $state(datasource)
let oldDatasource = datasource
$: if (datasource && datasource != oldDatasource) {
oldDatasource = datasource
api?.updateGridOptions({ datasource })
}
let extraConfig = resolvedConfig.extraConfig
$: if (!deepEqual(extraConfig, resolvedConfig.extraConfig)) {
extraConfig = resolvedConfig.extraConfig
if (extraConfig) {
api?.updateGridOptions(extraConfig)
}
}
let extraConfig = $state(resolvedConfig.extraConfig)
export function clearRows() {
api?.purgeInfiniteCache()
@@ -435,6 +437,33 @@
}
})
}
$effect(() => {
eGui && untrack(() => mountGrid())
})
$effect(() => {
actions && untrack(() => refreshActions(actions))
})
$effect(() => {
api && resolvedConfig && updateOptions()
})
$effect(() => {
if (api && datasource && datasource != oldDatasource) {
oldDatasource = datasource
untrack(() => {
api?.updateGridOptions({ datasource })
})
}
})
$effect(() => {
if (!deepEqual(extraConfig, resolvedConfig.extraConfig)) {
extraConfig = resolvedConfig.extraConfig
if (extraConfig && api) {
untrack(() => {
extraConfig && api?.updateGridOptions(extraConfig)
})
}
}
})
</script>
{#each Object.keys(css ?? {}) as key (key)}
@@ -459,19 +488,19 @@
bind:clientWidth
>
<div
on:pointerdown|stopPropagation={() => {
onpointerdown={stopPropagation(() => {
$selectedComponent = [id]
}}
})}
style:height="{clientHeight}px"
style:width="{clientWidth}px"
class="ag-theme-alpine"
class:ag-theme-alpine-dark={$darkMode}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={eGui}
style:height="100%"
on:keydown={(e) => {
onkeydown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && $mode !== 'dnd') {
const selectedCell = api?.getFocusedCell()
if (selectedCell) {
@@ -490,7 +519,9 @@
<div class="flex gap-1 w-full justify-between items-center text-xs text-primary p-2">
<div>
<Popover>
<svelte:fragment slot="text">Download</svelte:fragment>
{#snippet text()}
Download
{/snippet}
<Button
startIcon={{ icon: Download }}
color="light"
@@ -1,7 +1,9 @@
<script lang="ts">
import { stopPropagation } from 'svelte/legacy'
import { GridApi, createGrid } from 'ag-grid-community'
import { isObject, sendUserToast } from '$lib/utils'
import { getContext, mount, onDestroy, unmount } from 'svelte'
import { getContext, mount, onDestroy, unmount, untrack } from 'svelte'
import type { AppInput } from '../../../inputType'
import type {
AppViewerContext,
@@ -43,16 +45,28 @@
import InputValue from '../../helpers/InputValue.svelte'
import { stateSnapshot, withProps } from '$lib/svelte5Utils.svelte'
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
interface Props {
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
id: string
componentInput: AppInput | undefined
configuration: RichConfigurations
initializing?: boolean | undefined
render: boolean
customCss?: ComponentCustomCSS<'aggridcomponent'> | undefined
actions?: TableAction[] | undefined
actionsOrder?: RichConfiguration | undefined
}
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let initializing: boolean | undefined = undefined
export let render: boolean
export let customCss: ComponentCustomCSS<'aggridcomponent'> | undefined = undefined
export let actions: TableAction[] | undefined = undefined
export let actionsOrder: RichConfiguration | undefined = undefined
let {
id,
componentInput,
configuration,
initializing = $bindable(undefined),
render,
customCss = undefined,
actions = undefined,
actionsOrder = undefined
}: Props = $props()
const context = getContext<AppViewerContext>('AppViewerContext')
const contextPanel = getContext<ContextPanelContext>('ContextPanel')
@@ -68,12 +82,9 @@
comfortable: 50
}
let css = initCss($app.css?.aggridcomponent, customCss)
let css = $state(initCss($app.css?.aggridcomponent, customCss))
let result: any[] | undefined = undefined
$: resolvedConfig?.rowIdCol && resetValues()
$: result && setValues()
let result: any[] | undefined = $state(undefined)
function resetValues() {
api?.setGridOption('rowData', value)
@@ -82,11 +93,13 @@
let uid = Math.random().toString(36).substring(7)
let prevUid: string | undefined = undefined
let value: any[] = Array.isArray(result)
? (result as any[]).map((x, i) => ({ ...x, __index: i.toString() + '-' + uid }))
: [{ error: 'input was not an array' }]
let value: any[] = $state(
Array.isArray(result)
? (result as any[]).map((x, i) => ({ ...x, __index: i.toString() + '-' + uid }))
: [{ error: 'input was not an array' }]
)
let loaded = false
let loaded = $state(false)
async function setValues() {
value = Array.isArray(result)
@@ -108,9 +121,8 @@
}
}
let resolvedConfig = initConfig(
components['aggridcomponent'].initialData.configuration,
configuration
let resolvedConfig = $state(
initConfig(components['aggridcomponent'].initialData.configuration, configuration)
)
let outputs = initOutput($worldStore, id, {
@@ -170,10 +182,8 @@
}
}
$: outputs?.result?.set(result ?? [])
let clientHeight
let clientWidth
let clientHeight: number = $state(0)
let clientWidth: number = $state(0)
function onCellValueChanged(event) {
if (result) {
@@ -197,12 +207,10 @@
}
}
let extraConfig = deepCloneWithFunctions(resolvedConfig.extraConfig)
let api: GridApi<any> | undefined = undefined
let eGui: HTMLDivElement
let state: any = undefined
$: loaded && eGui && mountGrid()
let extraConfig = $state(deepCloneWithFunctions(resolvedConfig.extraConfig))
let api: GridApi<any> | undefined = $state(undefined)
let eGui: HTMLDivElement | undefined = $state(undefined)
let componentState: any = undefined
function refreshActions(actions: TableAction[]) {
if (!deepEqual(actions, lastActions)) {
@@ -212,19 +220,14 @@
}
let lastActions: TableAction[] | undefined = undefined
$: actions && refreshActions(actions)
let lastActionsOrder: string[] | undefined = undefined
$: computedOrder && refreshActionsOrder(computedOrder)
function clearActionOrder() {
computedOrder = undefined
updateOptions()
}
$: computedOrder && computedOrder.length > 0 && actionsOrder === undefined && clearActionOrder()
function refreshActionsOrder(actionsOrder: string[] | undefined) {
if (Array.isArray(actionsOrder) && !deepEqual(actionsOrder, lastActionsOrder)) {
lastActionsOrder = [...actionsOrder]
@@ -355,7 +358,7 @@
outputs?.page.set(event.api.paginationGetCurrentPage())
footerRenderCount++
},
initialState: state,
initialState: componentState,
suppressRowDeselection: true,
suppressDragLeaveHidesColumns: true,
enableCellTextSelection: true,
@@ -367,7 +370,7 @@
...resolvedConfig?.extraConfig?.['defaultColDef']
},
onStateUpdated: (e) => {
state = e?.api?.getState()
componentState = e?.api?.getState()
resolvedConfig?.extraConfig?.['onStateUpdated']?.(e)
},
@@ -441,16 +444,6 @@
}
}
$: api && resolvedConfig && updateOptions()
$: value && updateValue()
$: if (!deepEqual(extraConfig, resolvedConfig.extraConfig)) {
extraConfig = deepCloneWithFunctions(resolvedConfig.extraConfig)
if (extraConfig) {
api?.updateGridOptions(extraConfig)
}
}
function onSelectionChanged(api: GridApi<any>) {
if (resolvedConfig?.multipleSelectable) {
const rows = api.getSelectedNodes()
@@ -532,12 +525,52 @@
sendUserToast("Couldn't update the grid:" + e, true)
}
}
let loading = false
let refreshCount: number = 0
let footerRenderCount: number = 0
let computedOrder: string[] | undefined = undefined
let loading = $state(false)
let refreshCount: number = $state(0)
let footerRenderCount: number = $state(0)
let computedOrder: string[] | undefined = $state(undefined)
let footerHeight: number = 0
let footerHeight: number = $state(0)
$effect(() => {
resolvedConfig?.rowIdCol && untrack(() => resetValues())
})
$effect(() => {
result && untrack(() => setValues())
})
$effect(() => {
outputs?.result?.set(result ?? [])
})
$effect(() => {
loaded && eGui && untrack(() => mountGrid())
})
$effect(() => {
actions && refreshActions(actions)
})
$effect(() => {
computedOrder && untrack(() => refreshActionsOrder(computedOrder))
})
$effect(() => {
computedOrder &&
computedOrder.length > 0 &&
actionsOrder === undefined &&
untrack(() => clearActionOrder())
})
$effect(() => {
api && resolvedConfig && untrack(() => updateOptions())
})
$effect(() => {
value && untrack(() => updateValue())
})
$effect(() => {
if (!deepEqual(extraConfig, resolvedConfig.extraConfig)) {
extraConfig = deepCloneWithFunctions(resolvedConfig.extraConfig)
if (api && extraConfig) {
untrack(() => {
api?.updateGridOptions(extraConfig)
})
}
}
})
</script>
{#if actionsOrder}
@@ -591,9 +624,9 @@
{/if}
<div
on:pointerdown|stopPropagation={() => {
onpointerdown={stopPropagation(() => {
$selectedComponent = [id]
}}
})}
style:height="{clientHeight - (resolvedConfig.footer ? footerHeight : 0)}px"
style:width="{clientWidth}px"
class="ag-theme-alpine relative"
@@ -601,11 +634,11 @@
>
{#key resolvedConfig?.pagination}
{#if loaded}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={eGui}
style:height="100%"
on:keydown={(e) => {
onkeydown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && $mode !== 'dnd') {
const selectedCell = api?.getFocusedCell()
if (selectedCell) {
@@ -632,7 +665,9 @@
>
<div>
<Popover>
<svelte:fragment slot="text">Download</svelte:fragment>
{#snippet text()}
Download
{/snippet}
<Button
startIcon={{ icon: Download }}
color="light"
@@ -8,16 +8,27 @@
type T = Record<string, any>
export let result: Array<T>
export let manualPagination: boolean
export let pageSize: number
export let table: Readable<Table<T>>
export let download: boolean = true
export let loading: boolean = false
interface Props {
result: Array<T>
manualPagination: boolean
pageSize: number
table: Readable<Table<T>>
download?: boolean
loading?: boolean
class?: string
style?: string
}
let c = ''
export { c as class }
export let style = ''
let {
result,
manualPagination,
pageSize,
table,
download = true,
loading = false,
class: c = '',
style = ''
}: Props = $props()
function convertJSONToCSV(objArray: Record<string, any>[]) {
let str = ''
@@ -51,13 +62,15 @@
downloadAnchorNode.remove()
}
let isPreviousLoading = false
let isNextLoading = false
let isPreviousLoading = $state(false)
let isNextLoading = $state(false)
$: if (!loading) {
isPreviousLoading = false
isNextLoading = false
}
$effect(() => {
if (!loading) {
isPreviousLoading = false
isNextLoading = false
}
})
</script>
{#if result.length > pageSize || manualPagination || download}
@@ -68,7 +81,9 @@
<div class="flex items-center gap-1 flex-row">
{#if download}
<Popover>
<svelte:fragment slot="text">Download as CSV</svelte:fragment>
{#snippet text()}
Download as CSV
{/snippet}
<Button
size="xs2"
@@ -82,7 +97,9 @@
{/if}
{#if !$table.getIsAllColumnsVisible()}
<Popover>
<svelte:fragment slot="text">Display hidden columns</svelte:fragment>
{#snippet text()}
Display hidden columns
{/snippet}
<Button
size="xs2"
color="light"
@@ -8,10 +8,21 @@
import Alert from '$lib/components/common/alert/Alert.svelte'
import { isObject } from '$lib/utils'
export let id: string
export let columnDefs: Array<any> = []
export let result: Array<any> | undefined = []
export let allowColumnDefsActions: boolean = true
interface Props {
id: string
columnDefs?: Array<any>
result?: Array<any> | undefined
allowColumnDefsActions?: boolean
children?: import('svelte').Snippet
}
let {
id,
columnDefs = [],
result = [],
allowColumnDefsActions = true,
children
}: Props = $props()
const { app, mode, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
@@ -80,7 +91,7 @@
</Alert>
</div>
{:else}
<slot />
{@render children?.()}
{/if}
{:else if columnDefs !== undefined}
<div class="m-16">
@@ -4,25 +4,32 @@
import { classNames } from '$lib/utils'
import { Bug } from 'lucide-svelte'
export let hasError: boolean = false
interface Props {
hasError?: boolean
children?: import('svelte').Snippet
}
let { hasError = false, children }: Props = $props()
</script>
{#if hasError}
<div class={classNames('bg-red-100 w-full h-full flex items-center justify-center text-red-500')}>
<Popover notClickable placement="bottom" popupClass="!bg-surface border w-96">
<Bug size={14} />
<span slot="text">
<div class="bg-surface">
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
One of the configuration of the component is invalid. Please check the configuration
and try again.
</div>
</Alert>
</div>
</span>
{#snippet text()}
<span>
<div class="bg-surface">
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
One of the configuration of the component is invalid. Please check the configuration
and try again.
</div>
</Alert>
</div>
</span>
{/snippet}
</Popover>
</div>
{:else}
<slot />
{@render children?.()}
{/if}
@@ -2,7 +2,11 @@
import { LoaderIcon } from 'lucide-svelte'
import Popover from '$lib/components/Popover.svelte'
export let loading: boolean
interface Props {
loading: boolean
}
let { loading }: Props = $props()
</script>
{#if loading}
@@ -10,12 +14,12 @@
<div class={'bg-blue-100 dark:bg-blue-400 transition-all p-1 rounded-component'}>
<LoaderIcon size={14} class="animate-spin text-blue-800 dark:text-white" />
</div>
<svelte:fragment slot="text">
{#snippet text()}
{#if loading}
Refreshing...
{:else}
Refresh
{/if}
</svelte:fragment>
{/snippet}
</Popover>
{/if}
@@ -800,7 +800,7 @@
lastJobId = e.detail.id
setResult(e.detail.result, e.detail.id)
loading = false
dispatch('done', { id: e.detail.id, result: e.detail.result })
dispatch('done', { id: e.detail?.id, result: e.detail?.result })
}}
on:cancel={(e) => {
let jobId = e.detail
@@ -1,11 +1,8 @@
<script lang="ts">
import type { ComponentCustomCSS, RichConfigurations } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
export let id: string
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'multiselectcomponent'> | undefined = undefined
export let render: boolean
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
</script>
@@ -6,8 +6,12 @@
import { twMerge } from 'tailwind-merge'
import RecomputeAllButton from './RecomputeAllButton.svelte'
export let containerClass: string | undefined = undefined
export let containerStyle: string | undefined = undefined
interface Props {
containerClass?: string | undefined
containerStyle?: string | undefined
}
let { containerClass = undefined, containerStyle = undefined }: Props = $props()
const { connectingInput, bgRuns, recomputeAllContext } =
getContext<AppViewerContext>('AppViewerContext')
@@ -23,15 +27,17 @@
<span class="!text-2xs text-tertiary inline-flex gap-1 items-center"
><Loader2 size={10} class="animate-spin" /> {$bgRuns.length}
</span>
<span slot="text"
><div class="flex flex-col">
{#each $bgRuns as bgRun}
<div class="flex gap-2 items-center">
<div class="text-2xs">{bgRun}</div>
</div>
{/each}
</div></span
>
{#snippet text()}
<span
><div class="flex flex-col">
{#each $bgRuns as bgRun}
<div class="flex gap-2 items-center">
<div class="text-2xs">{bgRun}</div>
</div>
{/each}
</div></span
>
{/snippet}
</Popover>
{/if}
</div>
@@ -13,17 +13,7 @@
const { selectedComponent, app, stateId, runnableComponents } =
getContext<AppViewerContext>('AppViewerContext')
let firstComponent = $selectedComponent?.[0]
$: $selectedComponent?.[0] != firstComponent && (firstComponent = $selectedComponent?.[0])
$: hiddenInlineScript = $app?.hiddenInlineScripts
?.map((x, i) => ({ script: x, index: i }))
.find(({ script, index }) => $selectedComponent?.includes(BG_PREFIX + index))
$: gridItemWithLocation = findGridItemWithLocation($app, firstComponent)
$: tableActionSettings = findTableActionSettings($app, firstComponent)
$: menuItemsSettings = findMenuItemsSettings($app, firstComponent)
let firstComponent = $derived($selectedComponent?.[0])
function findTableActionSettings(app: App, id: string | undefined) {
return allItemsWithLocation(app.grid, app.subgrids)
@@ -107,6 +97,15 @@
}
const dispatch = createEventDispatcher()
let hiddenInlineScript = $derived(
$app?.hiddenInlineScripts
?.map((x, i) => ({ script: x, index: i }))
.find(({ script, index }) => $selectedComponent?.includes(BG_PREFIX + index))
)
let gridItemWithLocation = $derived(findGridItemWithLocation($app, firstComponent))
let tableActionSettings = $derived(findTableActionSettings($app, firstComponent))
let menuItemsSettings = $derived(findMenuItemsSettings($app, firstComponent))
</script>
{#if gridItemWithLocation}
@@ -351,13 +351,7 @@
{render}
/>
{:else if component.type === 'multiselectcomponent'}
<AppMultiSelect
id={component.id}
configuration={component.configuration}
customCss={component.customCss}
verticalAlignment={component.verticalAlignment}
{render}
/>
<AppMultiSelect id={component.id} verticalAlignment={component.verticalAlignment} {render} />
{:else if component.type === 'multiselectcomponentv2'}
<AppMultiSelectV2
id={component.id}
@@ -1,6 +1,6 @@
<script lang="ts">
import type { AppEditorContext, AppViewerContext } from '../../types'
import { getContext, tick } from 'svelte'
import { getContext, tick, untrack } from 'svelte'
import {
components as componentsRecord,
presets as presetsRecord,
@@ -34,12 +34,12 @@
let groups: Array<{
name: string
path: string
}> = []
}> = $state([])
let customComponents: Array<{
name: string
path: string
}> = []
}> = $state([])
async function fetchGroups() {
groups = await listGroups($workspaceStore ?? '')
@@ -164,30 +164,34 @@
$app = $app
}
let search = ''
let search = $state('')
$: componentsFiltered = COMPONENT_SETS.map((set) => ({
...set,
components: set.components?.filter((component) => {
const name = componentsRecord[component].name.toLowerCase()
return name.includes(search.toLowerCase().trim())
}),
presets: set.presets?.filter((preset) => {
const presetName = presetsRecord[preset].name.toLowerCase()
return presetName.includes(search.toLowerCase().trim())
})
}))
let componentsFiltered = $derived(
COMPONENT_SETS.map((set) => ({
...set,
components: set.components?.filter((component) => {
const name = componentsRecord[component].name.toLowerCase()
return name.includes(search.toLowerCase().trim())
}),
presets: set.presets?.filter((preset) => {
const presetName = presetsRecord[preset].name.toLowerCase()
return presetName.includes(search.toLowerCase().trim())
})
}))
)
$: {
$effect(() => {
if ($workspaceStore) {
fetchGroups()
fetchCustomComponents()
untrack(() => {
fetchGroups()
fetchCustomComponents()
})
}
}
})
let dndTimeout: NodeJS.Timeout | undefined = undefined
let dndTimeout: NodeJS.Timeout | undefined = $state(undefined)
let ccDrawer: Drawer
let ccDrawer: Drawer | undefined = $state()
</script>
<Drawer bind:this={ccDrawer}>
@@ -217,22 +221,25 @@
<ListItem title={`${title}`} subtitle={`(${components.length})`}>
<div class="flex flex-wrap gap-3 py-2">
{#each components as item (item)}
{@const SvelteComponent = componentsRecord[item].icon}
<div class="w-[64px] relative">
{#if DEPRECATED_COMPONENTS[item]}
<div
class="absolute -top-2 -right-2 bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100 rounded-md py-0.5 px-1 flex flex-row gap-1 items-center"
>
<Popover>
<div slot="text">
{DEPRECATED_COMPONENTS[item]}
</div>
{#snippet text()}
<div>
{DEPRECATED_COMPONENTS[item]}
</div>
{/snippet}
<div class="font-normal text-2xs"> Deprecated </div>
</Popover>
</div>
{/if}
<button
id={item}
on:pointerdown={async (e) => {
onpointerdown={async (e) => {
e.preventDefault()
const id = addComponent(item)
dndTimeout && clearTimeout(dndTimeout)
@@ -249,7 +256,7 @@
class="cursor-move transition-all border w-[64px] shadow-sm h-16 p-2 flex flex-col gap-2 items-center
justify-center bg-surface rounded-md hover:bg-blue-50 dark:hover:bg-blue-900 duration-200 hover:border-blue-500"
>
<svelte:component this={componentsRecord[item].icon} class="text-primary" />
<SvelteComponent class="text-primary" />
</button>
<div class="text-xs text-center flex-wrap text-secondary mt-1">
{componentsRecord[item].name}
@@ -258,17 +265,15 @@
{/each}
{#if presets}
{#each presets as presetItem (presetItem)}
{@const SvelteComponent_1 = presetsRecord[presetItem].icon}
<div class="w-[64px]">
<button
on:click={() => addPresetComponent(presetItem)}
onclick={() => addPresetComponent(presetItem)}
title={presetsRecord[presetItem].name}
class="transition-all border w-[64px] shadow-sm h-16 p-2 flex flex-col gap-2 items-center
justify-center bg-surface rounded-md hover:bg-blue-50 dark:hover:bg-blue-900 duration-200 hover:border-blue-500"
>
<svelte:component
this={presetsRecord[presetItem].icon}
class="text-secondary"
/>
<SvelteComponent_1 class="text-secondary" />
</button>
<div class="text-xs text-center flex-wrap text-secondary mt-1">
{presetsRecord[presetItem].name}
@@ -287,7 +292,7 @@
{#each groups as group (group.path)}
<div class="w-[64px]">
<button
on:click={() => {
onclick={() => {
addGroup(group)
}}
title={group.name}
@@ -304,7 +309,7 @@
{/if}
<div class="w-[64px]">
<button
on:click={() => {
onclick={() => {
addNewGroup()
}}
title=""
@@ -323,7 +328,7 @@
{#each customComponents as cc (cc.path)}
<div class="w-[64px]">
<button
on:click={() => {
onclick={() => {
addCustomComponent(cc)
}}
title={cc.name}
@@ -340,11 +345,11 @@
{/if}
<div class="w-[64px]">
<button
on:click={() => {
onclick={() => {
if (!$enterpriseLicense) {
sendUserToast('Custom components are only available on the EE', true)
} else {
ccDrawer.openDrawer()
ccDrawer?.openDrawer()
}
}}
title=""
@@ -5,8 +5,12 @@
import type { AppViewerContext, GridItem } from '../../types'
import { dfs, findGridItem } from '../appUtils'
export let type: string
export let id: string
interface Props {
type: string
id: string
}
let { type, id }: Props = $props()
const { app } = getContext<AppViewerContext>('AppViewerContext')
@@ -103,9 +107,9 @@
<div class="flex flex-row gap-1">
{#each contextVariables as contextVariable}
<Popover>
<svelte:fragment slot="text">
{#snippet text()}
{contextVariable.description}
</svelte:fragment>
{/snippet}
<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border">
{contextVariable.label}
@@ -5,19 +5,23 @@
import { BookText, ExternalLink } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
export let docLink: string
export let btnClasses: string | undefined = undefined
export let size: ButtonType.Size = 'xs'
interface Props {
docLink: string
btnClasses?: string | undefined
size?: ButtonType.Size
}
let { docLink, btnClasses = undefined, size = 'xs' }: Props = $props()
</script>
<Popover>
<svelte:fragment slot="text">
{#snippet text()}
<div class="flex flex-row gap-1">
Open documentation in a new tab
<ExternalLink size={16} />
</div>
</svelte:fragment>
{/snippet}
<Button
iconOnly
startIcon={{
@@ -289,13 +289,13 @@
/>
<div class="absolute top-1 right-1">
<AgGridWizard bind:value={componentInput.value}>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
{/snippet}
</AgGridWizard>
</div>
</div>
@@ -311,13 +311,13 @@
/>
<div class="absolute top-1 right-1">
<DBExplorerWizard bind:value={componentInput.value}>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
{/snippet}
</DBExplorerWizard>
</div>
</div>
@@ -332,13 +332,13 @@
/>
<div class="absolute top-1 right-1">
<TableColumnWizard bind:column={componentInput.value}>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
{/snippet}
</TableColumnWizard>
</div>
</div>
@@ -353,13 +353,13 @@
/>
<div class="absolute top-1 right-1">
<PlotlyWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
{/snippet}
</PlotlyWizard>
</div>
</div>
@@ -374,13 +374,13 @@
/>
<div class="absolute top-1 right-1">
<ChartJSWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
{/snippet}
</ChartJSWizard>
</div>
</div>
@@ -396,13 +396,13 @@
<div class="absolute top-1 right-1">
<AgChartWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
{/snippet}
</AgChartWizard>
</div>
</div>
@@ -10,19 +10,23 @@
callback: () => void
}
export let actions: ActionType[] = []
interface Props {
actions?: ActionType[]
}
let { actions = [] }: Props = $props()
</script>
<div class="flex flex-row gap-1 justify-end">
{#each actions as action, index (index)}
<Popover notClickable disappearTimeout={0}>
<svelte:fragment slot="text">
{#snippet text()}
{action.label}
</svelte:fragment>
{/snippet}
<Button color={action.color} on:click={action.callback} size="xs2" variant="border">
<div class="flex flex-row gap-1 items-center">
{#if action.icon}
<svelte:component this={action.icon} size={12} />
<action.icon size={12} />
{/if}
</div>
</Button>
@@ -12,24 +12,47 @@
import { slide } from 'svelte/transition'
import { twMerge } from 'tailwind-merge'
export let type: AlertType = 'info'
export let title: string
export let notRounded = false
export let tooltip: string = ''
export let documentationLink: string | undefined = undefined
export let size: 'xs' | 'sm' = 'sm'
export let collapsible: boolean = false
interface Props {
type?: AlertType
title: string
notRounded?: boolean
tooltip?: string
documentationLink?: string | undefined
size?: 'xs' | 'sm'
collapsible?: boolean
bgClass?: string | undefined
bgStyle?: string | undefined
iconClass?: string | undefined
iconStyle?: string | undefined
titleClass?: string | undefined
titleStyle?: string | undefined
descriptionClass?: string | undefined
descriptionStyle?: string | undefined
class?: string | undefined
isCollapsed?: boolean
children?: import('svelte').Snippet
}
export let bgClass: string | undefined = undefined
export let bgStyle: string | undefined = undefined
export let iconClass: string | undefined = undefined
export let iconStyle: string | undefined = undefined
export let titleClass: string | undefined = undefined
export let titleStyle: string | undefined = undefined
export let descriptionClass: string | undefined = undefined
export let descriptionStyle: string | undefined = undefined
export let isCollapsed = true
let {
type = 'info',
title,
notRounded = false,
tooltip = '',
documentationLink = undefined,
size = 'sm',
collapsible = false,
bgClass = undefined,
bgStyle = undefined,
iconClass = undefined,
iconStyle = undefined,
titleClass = undefined,
titleStyle = undefined,
descriptionClass = undefined,
descriptionStyle = undefined,
class: classNames = undefined,
isCollapsed = $bindable(true),
children
}: Props = $props()
const icons: Record<AlertType, any> = {
info: Info,
@@ -43,6 +66,8 @@
isCollapsed = !isCollapsed
}
}
const SvelteComponent = $derived(icons[type])
</script>
<div
@@ -51,14 +76,13 @@
size === 'sm' ? 'p-4' : 'p-2',
classes[type].bgClass,
bgClass,
$$props.class
classNames
)}
style={bgStyle}
>
<div class="flex">
<div class="flex h-8 w-8 items-center justify-center rounded-full">
<svelte:component
this={icons[type]}
<SvelteComponent
class={twMerge(classes[type].iconClass, iconClass)}
style={iconStyle}
size={16}
@@ -77,11 +101,11 @@
>
{title}
{#if tooltip != '' || documentationLink}
<Tooltip {documentationLink} scale={0.9}>{tooltip}</Tooltip>
<Tooltip {documentationLink}>{tooltip}</Tooltip>
{/if}
</span>
{#if collapsible}
<button class="cursor-pointer" on:click={toggleCollapse}>
<button class="cursor-pointer" onclick={toggleCollapse}>
{#if isCollapsed}
<ChevronDown size={16} />
{:else}
@@ -91,7 +115,7 @@
{/if}
</div>
{#if $$slots.default && !isCollapsed}
{#if children && !isCollapsed}
<div transition:slide|local={{ duration: 200 }} class="mt-2">
<div
class={twMerge(
@@ -101,10 +125,10 @@
)}
style={descriptionStyle}
>
<slot />
{@render children?.()}
</div>
</div>
{:else if $$slots.default && !collapsible}
{:else if children && !collapsible}
<div class="mb-2">
<div
class={twMerge(
@@ -114,7 +138,7 @@
)}
style={descriptionStyle}
>
<slot />
{@render children?.()}
</div>
</div>
{/if}
@@ -1,4 +1,4 @@
<script context="module" lang="ts">
<script module lang="ts">
export type ConnectionInfo = {
connected: boolean
message?: string
@@ -9,7 +9,11 @@
import Popover from '$lib/components/Popover.svelte'
import { Circle } from 'lucide-svelte'
export let connectionInfo: ConnectionInfo | undefined = undefined
interface Props {
connectionInfo?: ConnectionInfo | undefined
}
let { connectionInfo = undefined }: Props = $props()
</script>
{#if connectionInfo}
@@ -20,7 +24,9 @@
<Circle class="text-green-600 relative inline-flex fill-current" size={12} />
</span>
<div slot="text"> {connectionInfo.message ?? ''} </div>
{#snippet text()}
<div> {connectionInfo.message ?? ''} </div>
{/snippet}
</Popover>
{:else}
<Popover notClickable>
@@ -29,7 +35,9 @@
<Circle class="text-red-600 relative inline-flex fill-current" size={12} />
</span>
<div slot="text"> {connectionInfo.message ?? ''} </div>
{#snippet text()}
<div> {connectionInfo.message ?? ''} </div>
{/snippet}
</Popover>
{/if}
</div>
@@ -4,9 +4,13 @@
import Popover from '$lib/components/Popover.svelte'
export let loading: boolean
interface Props {
loading: boolean
}
let buttonHover = false
let { loading }: Props = $props()
let buttonHover = $state(false)
</script>
<Popover>
@@ -20,7 +24,7 @@
>
<RefreshCw class={loading ? 'animate-spin ' : ''} size="14" />
</Button>
<svelte:fragment slot="text">
{#snippet text()}
{#if loading}
{#if buttonHover}
Stop Refreshing
@@ -30,5 +34,5 @@
{:else}
Refresh
{/if}
</svelte:fragment>
{/snippet}
</Popover>
@@ -41,19 +41,21 @@
<div class={classNames('flex flex-col divide-y', fullScreen ? 'h-screen max-h-screen' : 'h-full')}>
<div class="flex justify-between w-full items-center px-4 py-2 gap-2">
<div class="flex items-center gap-2 w-full truncate">
<div use:triggerableByAI={{
id: `close-${aiId}`,
description: `Close ${aiDescription}`,
callback: () => {
dispatch('close')
}
}}>
<div
use:triggerableByAI={{
id: `close-${aiId}`,
description: `Close ${aiDescription}`,
callback: () => {
dispatch('close')
}
}}
>
<CloseButton on:close Icon={CloseIcon} />
</div>
<span class="font-semibold truncate text-primary !text-lg max-w-sm"
>{title ?? ''}
{#if tooltip != '' || documentationLink}
<Tooltip {documentationLink} scale={0.9}>{tooltip}</Tooltip>
<Tooltip {documentationLink}>{tooltip}</Tooltip>
{/if}</span
>
</div>
@@ -4,11 +4,19 @@
import { AlertTriangle, Hourglass } from 'lucide-svelte'
import Badge from '../badge/Badge.svelte'
export let self_wait_time_ms: number | undefined = undefined
export let aggregate_wait_time_ms: number | undefined = undefined
export let variant: 'icon' | 'alert' | 'badge' | 'badge-self-wait' = 'icon'
interface Props {
self_wait_time_ms?: number | undefined
aggregate_wait_time_ms?: number | undefined
variant?: 'icon' | 'alert' | 'badge' | 'badge-self-wait'
}
$: total_wait = (self_wait_time_ms ?? 0) + (aggregate_wait_time_ms ?? 0)
let {
self_wait_time_ms = undefined,
aggregate_wait_time_ms = undefined,
variant = 'icon'
}: Props = $props()
let total_wait = $derived((self_wait_time_ms ?? 0) + (aggregate_wait_time_ms ?? 0))
function classFromColorName(color: string): string | undefined {
const colors: Record<string, string> = {
@@ -36,7 +44,7 @@
</script>
<Popover notClickable>
<svelte:fragment slot="text">
{#snippet text()}
<div class="mb-5">
{#if self_wait_time_ms != undefined}
<div>
@@ -66,7 +74,7 @@
</div>
{/if}
<div> In a healthy queue, jobs are expected to start in under 50ms. </div>
</svelte:fragment>
{/snippet}
{#if variant === 'icon'}
<Hourglass class={classFromColorName(waitColorTresholds(total_wait))} size={14} />
{:else if variant === 'badge'}
@@ -75,9 +83,7 @@
>
{:else if variant === 'badge-self-wait'}
{#if self_wait_time_ms}
<Badge
color={waitColorTresholds(self_wait_time_ms)}>+{msToSec(self_wait_time_ms)}s</Badge
>
<Badge color={waitColorTresholds(self_wait_time_ms)}>+{msToSec(self_wait_time_ms)}s</Badge>
{/if}
{:else if variant === 'alert'}
<AlertTriangle class={classFromColorName(waitColorTresholds(total_wait))} size={14} />
@@ -8,18 +8,20 @@
import { getModifierKey } from '$lib/utils'
import { WandSparkles } from 'lucide-svelte'
let { openPanel }: { openPanel: () => void } = $props()
let { togglePanel, opened }: { togglePanel: () => void; opened?: boolean } = $props()
</script>
{#snippet button(onClick: () => void)}
<Button
color="light"
variant="border"
size="xs"
size="xs2"
on:click={onClick}
startIcon={{ icon: WandSparkles }}
iconOnly
btnClasses="!text-violet-800 dark:!text-violet-400 border border-gray-200 dark:border-gray-600 bg-surface"
btnClasses="!text-violet-800 dark:!text-violet-400 border border-gray-200 dark:border-gray-600 bg-surface h-[28px] w-[34px] rounded-sm py-1 px-2 {opened
? 'bg-surface-selected'
: ''}"
>
AI Panel
</Button>
@@ -27,7 +29,7 @@
{#if $copilotInfo.enabled}
<DarkPopover>
<svelte:fragment slot="text">
{#snippet text()}
<div class="flex flex-row gap-1">
Show the AI Panel.
@@ -35,15 +37,17 @@
{getModifierKey()}L
</div>
</div>
</svelte:fragment>
{@render button(openPanel)}
{/snippet}
{@render button(togglePanel)}
</DarkPopover>
{:else}
<Popover placement="bottom">
<svelte:fragment slot="trigger">
{@render button(() => {})}
</svelte:fragment>
<svelte:fragment slot="content">
{#snippet trigger()}
{@render button(() => {
togglePanel()
})}
{/snippet}
{#snippet content()}
<div class="block text-primary p-4">
<p class="text-sm"
>Enable Windmill AI in the <a
@@ -54,6 +58,6 @@
></p
>
</div>
</svelte:fragment>
{/snippet}
</Popover>
{/if}
@@ -26,17 +26,20 @@
let {
id,
action
action,
placement = 'top'
}: {
id: string | undefined
action: AIModuleAction | undefined
placement?: 'top' | 'bottom'
} = $props()
</script>
{#if action && id}
<div
class={twMerge(
'absolute right-0 left-0 top-0 -translate-y-full flex flex-row ',
'absolute right-0 left-0 flex flex-row ',
placement === 'top' ? 'top-0 -translate-y-full' : 'bottom-0 translate-y-full',
action === 'modified' ? 'justify-between' : 'justify-end'
)}
>
@@ -50,7 +53,12 @@
<DiffIcon size={14} /> Diff
</button>
{/if}
<div class="rounded-t-md flex flex-row bg-surface overflow-hidden">
<div
class={twMerge(
'flex flex-row bg-surface overflow-hidden',
placement === 'top' ? 'rounded-t-md' : 'rounded-b-md'
)}
>
<button
class="p-1 bg-green-500 text-white hover:bg-green-600 text-3xs font-normal flex flex-row items-center gap-1"
onclick={() => aiChatManager.flowAiChatHelpers?.acceptModuleAction(id)}
+1
View File
@@ -10,6 +10,7 @@ export type FlowBuilderWhitelabelCustomUi = {
diff?: boolean
extraDeployOptions?: boolean
editableSummary?: boolean
settings?: boolean
}
settingsPanel?: boolean
settingsTabs?: {
@@ -36,6 +36,9 @@
onEditInput?: ((moduleId: string, key: string) => void) | undefined
forceTestTab?: Record<string, boolean>
highlightArg?: Record<string, string | undefined>
aiChatOpen?: boolean
showFlowAiButton?: boolean
toggleAiChat?: () => void
onRunPreview?: () => void
}
@@ -54,6 +57,9 @@
onEditInput = undefined,
forceTestTab,
highlightArg,
aiChatOpen,
showFlowAiButton,
toggleAiChat,
onRunPreview = () => {}
}: Props = $props()
@@ -108,6 +114,9 @@
}}
{onTestUpTo}
{onEditInput}
{aiChatOpen}
{showFlowAiButton}
{toggleAiChat}
/>
{/if}
</div>
@@ -76,7 +76,7 @@
<div class="p-4 h-full flex flex-col" id="flow-editor-flow-inputs">
{#if summary == 'Terminate flow'}
<Alert role="info" title="The flow stops here"
<Alert type="info" title="The flow stops here"
>This is an identity step with an early stop that has 'true' for expression</Alert
>
{:else}{#if !failureModule && !preprocessorModule}
@@ -114,7 +114,7 @@
{/if}
{#if kind == 'trigger'}
<div class="mt-2"></div>
<Alert title="Trigger scripts" role="info">
<Alert title="Trigger scripts" type="info">
Trigger scripts are designed to pull data from an external source and return all of the new
items since the last run, without resorting to external webhooks.<br /><br />
@@ -149,7 +149,7 @@
{#if kind == 'script' && !noEditor && !preprocessorModule}
<div class="mt-2"></div>
<Alert title="Action Scripts" role="info">
<Alert title="Action Scripts" type="info">
An action script is simply a script that is neither a trigger nor an approval script. Those
are the majority of the scripts.
</Alert>
@@ -158,7 +158,7 @@
{#if kind == 'approval'}
{#if !noEditor}
<div class="mt-2"></div>
<Alert title="Approval/Prompt Step" role="info">
<Alert title="Approval/Prompt Step" type="info">
An approval/prompt step will suspend the execution of a flow until it has been approved
and/or the prompts have been filled in the UI or through the resume endpoints or the
approval page by and solely by the recipients of the secret urls. See details in
@@ -6,7 +6,7 @@
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import { defaultScripts, enterpriseLicense, userStore } from '$lib/stores'
import type { SupportedLanguage } from '$lib/common'
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
import { createEventDispatcher, getContext, onDestroy, onMount, untrack } from 'svelte'
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
import PickHubScriptQuick from '../pickers/PickHubScriptQuick.svelte'
import { type Script, type ScriptLang, type HubScriptKind } from '$lib/gen'
@@ -26,18 +26,33 @@
const dispatch = createEventDispatcher()
export let summary: string | undefined = undefined
export let filter = ''
export let disableAi = false
export let preFilter: 'all' | 'workspace' | 'hub' = 'hub'
export let funcDesc: string
export let owners: string[] = []
export let loading = false
export let small = false
export let kind: 'trigger' | 'script' | 'preprocessor' | 'failure' | 'approval'
export let selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure' =
kind
export let displayPath = false
interface Props {
summary?: string | undefined
filter?: string
disableAi?: boolean
preFilter?: 'all' | 'workspace' | 'hub'
funcDesc: string
owners?: string[]
loading?: boolean
small?: boolean
kind: 'trigger' | 'script' | 'preprocessor' | 'failure' | 'approval'
selectedKind?: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
displayPath?: boolean
}
let {
summary = undefined,
filter = $bindable(''),
disableAi = false,
preFilter = 'hub',
funcDesc,
owners = $bindable([]),
loading = $bindable(false),
small = false,
kind,
selectedKind = kind,
displayPath = false
}: Props = $props()
type HubCompletion = {
path: string
@@ -49,26 +64,21 @@
kind: HubScriptKind
}
let lang: ScriptLang | undefined = undefined
let lang: ScriptLang | undefined = $state(undefined)
let filteredWorkspaceItems: (Script & { marked?: string })[] = []
let filteredWorkspaceItems: (Script & { marked?: string })[] = $state([])
let hubCompletions: HubCompletion[] = []
let hubCompletions: HubCompletion[] = $state([])
const { insertButtonOpen } = getContext<FlowEditorContext>('FlowEditorContext')
let selected: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined = undefined
let selected: { kind: 'owner' | 'integrations'; name: string | undefined } | undefined =
$state(undefined)
let integrations: string[] = []
let integrations: string[] = $state([])
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
$: langs = processLangs(undefined, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
.map((l) => [defaultScriptLanguages[l], l])
.filter(
(x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1])
) as [string, SupportedLanguage | 'docker'][]
function displayLang(
lang: SupportedLanguage | 'docker',
kind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
@@ -109,11 +119,9 @@
})
}
let openScriptSettings = false
let openScriptSettings = $state(false)
let selectedByKeyboard = 0
$: onSelectedKindChange(selectedKind)
let selectedByKeyboard = $state(0)
function onSelectedKindChange(
_selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure'
@@ -121,7 +129,7 @@
selectedByKeyboard = 0
}
let inlineScripts: [string, SupportedLanguage | 'docker'][] = []
let inlineScripts: [string, SupportedLanguage | 'docker'][] = $state([])
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql', 'oracledb']
@@ -153,7 +161,7 @@
['Branch to one', 'branchone'],
['Branch to all', 'branchall']
]
let topLevelNodes: [string, string][] = []
let topLevelNodes: [string, string][] = $state([])
function computeToplevelNodeChoices(funcDesc: string, preFilter: 'all' | 'workspace' | 'hub') {
if (funcDesc.length > 0 && preFilter == 'all' && kind == 'script') {
topLevelNodes = allToplevelNodes.filter((node) =>
@@ -164,10 +172,6 @@
}
}
$: computeToplevelNodeChoices(funcDesc, preFilter)
$: computeInlineScriptChoices(funcDesc, selected, preFilter, selectedKind)
$: onPrefilterChange(preFilter)
function onPrefilterChange(preFilter: 'all' | 'workspace' | 'hub') {
if (preFilter == 'workspace') {
hubCompletions = []
@@ -177,10 +181,7 @@
selectedByKeyboard = 0
}
$: aiLength =
funcDesc?.length > 0 && !disableAi && selectedKind != 'flow' && preFilter == 'all' ? 2 : 0
let scrollable: Scrollable | undefined
let scrollable: Scrollable | undefined = $state()
function onKeyDown(e: KeyboardEvent) {
let length =
topLevelNodes?.length +
@@ -206,9 +207,35 @@
onDestroy(() => {
$insertButtonOpen = false
})
let langs = $derived(
processLangs(undefined, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
.map((l) => [defaultScriptLanguages[l], l])
.filter(
(x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1])
) as [string, SupportedLanguage | 'docker'][]
)
$effect(() => {
selectedKind
untrack(() => onSelectedKindChange(selectedKind))
})
$effect(() => {
;[funcDesc, preFilter]
untrack(() => computeToplevelNodeChoices(funcDesc, preFilter))
})
$effect(() => {
;[funcDesc, selected, preFilter, selectedKind]
untrack(() => computeInlineScriptChoices(funcDesc, selected, preFilter, selectedKind))
})
$effect(() => {
preFilter
untrack(() => onPrefilterChange(preFilter))
})
let aiLength = $derived(
funcDesc?.length > 0 && !disableAi && selectedKind != 'flow' && preFilter == 'all' ? 2 : 0
)
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<div class="flex flex-row grow min-w-0 divide-x relative {!small ? 'shadow-inset' : ''}">
{#if selectedKind != 'preprocessor'}
<Scrollable shiftedShadow scrollableClass="w-32 grow-0 shrink-0 ">
@@ -230,7 +257,7 @@
'w-full text-left text-2xs text-primary font-normal py-2 px-3 hover:bg-surface-hover transition-all whitespace-nowrap flex flex-row gap-2 items-center rounded-md',
owner === selected?.name ? 'bg-surface-hover' : ''
)}
on:click={() => {
onclick={() => {
selected = selected?.name == owner ? undefined : { kind: 'owner', name: owner }
}}
>
@@ -274,7 +301,7 @@
'w-full text-left text-2xs text-primary font-normal py-2 px-3 hover:bg-surface-hover transition-all whitespace-nowrap flex flex-row gap-2 items-center rounded-md',
owner === selected?.name ? 'bg-surface-hover' : ''
)}
on:click={() => {
onclick={() => {
selected = selected?.name == owner ? undefined : { kind: 'owner', name: owner }
}}
>
@@ -23,8 +23,12 @@
import type { FlowBuilderWhitelabelCustomUi } from '$lib/components/custom_ui'
import FlowModuleWorkerTagSelect from './FlowModuleWorkerTagSelect.svelte'
export let module: FlowModule
export let tag: string | undefined
interface Props {
module: FlowModule
tag: string | undefined
}
let { module, tag }: Props = $props()
const { scriptEditorDrawer } = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
@@ -37,70 +41,84 @@
<Popover
placement="bottom"
class="center-center rounded p-2 bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200 dark:bg-frost-700 dark:text-frost-100 dark:border-frost-600"
on:click={() => dispatch('toggleRetry')}
onClick={() => dispatch('toggleRetry')}
>
<Repeat size={14} />
<svelte:fragment slot="text">Retries</svelte:fragment>
{#snippet text()}
Retries
{/snippet}
</Popover>
{/if}
{#if module?.value?.['concurrent_limit'] != undefined}
<Popover
placement="bottom"
class="center-center rounded p-2 bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200 dark:bg-frost-700 dark:text-frost-100 dark:border-frost-600"
on:click={() => dispatch('toggleConcurrency')}
onClick={() => dispatch('toggleConcurrency')}
>
<Gauge size={14} />
<svelte:fragment slot="text">Concurrency Limits</svelte:fragment>
{#snippet text()}
Concurrency Limits
{/snippet}
</Popover>
{/if}
{#if module.cache_ttl != undefined}
<Popover
placement="bottom"
class="center-center rounded p-2 bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200 dark:bg-frost-700 dark:text-frost-100 dark:border-frost-600"
on:click={() => dispatch('toggleCache')}
onClick={() => dispatch('toggleCache')}
>
<Database size={14} />
<svelte:fragment slot="text">Cache</svelte:fragment>
{#snippet text()}
Cache
{/snippet}
</Popover>
{/if}
{#if module.stop_after_if || module.stop_after_all_iters_if}
<Popover
placement="bottom"
class="center-center rounded p-2 bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200 dark:bg-frost-700 dark:text-frost-100 dark:border-frost-600"
on:click={() => dispatch('toggleStopAfterIf')}
onClick={() => dispatch('toggleStopAfterIf')}
>
<Square size={14} />
<svelte:fragment slot="text">Early stop/break</svelte:fragment>
{#snippet text()}
Early stop/break
{/snippet}
</Popover>
{/if}
{#if module.suspend}
<Popover
placement="bottom"
class="center-center rounded p-2 bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200 dark:bg-frost-700 dark:text-frost-100 dark:border-frost-600"
on:click={() => dispatch('toggleSuspend')}
onClick={() => dispatch('toggleSuspend')}
>
<PhoneIncoming size={14} />
<svelte:fragment slot="text">Suspend</svelte:fragment>
{#snippet text()}
Suspend
{/snippet}
</Popover>
{/if}
{#if module.sleep}
<Popover
placement="bottom"
class="center-center rounded p-2 bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200 dark:bg-frost-700 dark:text-frost-100 dark:border-frost-600"
on:click={() => dispatch('toggleSleep')}
onClick={() => dispatch('toggleSleep')}
>
<Bed size={14} />
<svelte:fragment slot="text">Sleep</svelte:fragment>
{#snippet text()}
Sleep
{/snippet}
</Popover>
{/if}
{#if module.mock?.enabled}
<Popover
placement="bottom"
class="center-center rounded p-2 bg-blue-100 text-blue-800 border border-blue-300 hover:bg-blue-200 dark:bg-frost-700 dark:text-frost-100 dark:border-frost-600"
on:click={() => dispatch('togglePin')}
onClick={() => dispatch('togglePin')}
>
<Pin size={14} />
<svelte:fragment slot="text">This step is pinned</svelte:fragment>
{#snippet text()}
This step is pinned
{/snippet}
</Popover>
{/if}
{/if}
@@ -111,7 +129,7 @@
<Button
size="xs"
color="light"
on:click={async () => {
onClick={async () => {
if (module.value.type == 'script') {
const hash = module.value.hash ?? (await getLatestHashForScript(module.value.path))
$scriptEditorDrawer?.openDrawer(hash, () => {
@@ -1,23 +0,0 @@
<script lang="ts">
import type { FlowEditorContext } from '../types'
import { getContext } from 'svelte'
import { classNames } from '$lib/utils'
import { DollarSign } from 'lucide-svelte'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
$: settingsClass = classNames(
'border w-full rounded-sm p-2 bg-surface text-sm cursor-pointer flex items-center',
$selectedId == 'constants'
? 'border border-1 border-slate-800 dark:bg-white/5 dark:border-slate-400/60 dark:border-gray-400'
: '',
'hover:!bg-surface-secondary active:!bg-surface'
)
</script>
<button on:click={() => ($selectedId = 'constants')} class={settingsClass}>
<DollarSign size={16} />
<span class="text-xs flex flex-row justify-between w-full gap-2 items-center truncate ml-1">
All Static Inputs
</span>
</button>
@@ -13,9 +13,11 @@
getAiModuleAction
} from '$lib/components/copilot/chat/flow/ModuleAcceptReject.svelte'
let {
small
small,
clazz
}: {
small: boolean
clazz: string
} = $props()
const dispatch = createEventDispatcher<{
@@ -50,69 +52,73 @@
const action = $derived(getAiModuleAction('failure'))
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
id="flow-editor-error-handler"
class={classNames(
'z-10',
'relative cursor-pointer border transition-colors duration-[400ms] ease-linear rounded-sm px-2 py-1 gap-2 bg-surface text-sm flex items-center flex-row',
$selectedId?.includes('failure')
? 'outline outline-offset-1 outline-2 outline-slate-900 dark:outline-slate-900/0 dark:bg-surface-secondary dark:border-gray-400'
: '',
aiModuleActionToBgColor(action)
)}
style="min-width: {small ? '200px' : '230px'}; max-width: 275px;"
onclick={() => {
if (flowStore.val?.value?.failure_module) {
$selectedId = 'failure'
}
}}
>
<ModuleAcceptReject id="failure" {action} />
<div class="flex items-center grow-0 min-w-0 gap-2">
<Bug size={16} color={flowStore.val?.value?.failure_module ? '#3b82f6' : '#9CA3AF'} />
</div>
{#if flowStore.val?.value?.failure_module}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
id="flow-editor-error-handler"
class={classNames(
'z-10',
'relative cursor-pointer border transition-colors duration-[400ms] ease-linear rounded-sm px-2 py-1 gap-2 bg-surface text-sm flex items-center flex-row',
$selectedId?.includes('failure')
? 'outline outline-offset-1 outline-2 outline-slate-900 dark:outline-slate-900/0 dark:bg-surface-secondary dark:border-gray-400'
: '',
aiModuleActionToBgColor(action)
)}
style="min-width: {flowStore.val?.value?.failure_module
? small
? '200px'
: '230px'
: ''}; max-width: 275px;"
onclick={() => {
if (flowStore.val?.value?.failure_module) {
$selectedId = 'failure'
}
}}
>
<ModuleAcceptReject id="failure" {action} placement="bottom" />
<div class="flex items-center grow-0 min-w-0 gap-2">
<Bug size={16} color={'#3b82f6'} />
</div>
{#if !flowStore.val?.value?.failure_module}
<div class="grow text-center font-bold text-xs">Error Handler</div>
{:else}
<div class="truncate grow min-w-0 text-center text-xs">
{flowStore.val.value.failure_module?.summary ||
(flowStore.val.value.failure_module?.value.type === 'rawscript'
? `${flowStore.val.value.failure_module?.value.language}`
: 'TBD')}
</div>
{/if}
{#if !flowStore.val?.value?.failure_module}
<InsertModuleButton
index={0}
placement={'top-center'}
on:new={(e) => {
insertFailureModule(e.detail.inlineScript)
}}
on:pickScript={(e) => {
insertFailureModule(undefined, e.detail)
}}
kind="failure"
/>
{:else if !action}
<button
title="Delete failure script"
type="button"
class={twMerge(
'w-5 h-5 flex items-center justify-center grow-0 shrink-0',
'outline-[1px] outline dark:outline-gray-500 outline-gray-300',
'text-secondary',
'bg-surface focus:outline-none hover:bg-surface-hover rounded '
)}
onclick={() => {
flowStore.val.value.failure_module = undefined
$selectedId = 'settings-metadata'
}}
>
<X size={12} />
</button>
{/if}
</div>
{#if !action}
<button
title="Delete failure script"
type="button"
class={twMerge(
'w-5 h-4 flex items-center justify-center grow-0 shrink-0',
'outline-[1px] outline dark:outline-gray-500 outline-gray-300',
'text-secondary',
'bg-surface focus:outline-none hover:bg-surface-hover rounded '
)}
onclick={() => {
flowStore.val.value.failure_module = undefined
$selectedId = 'settings-metadata'
}}
>
<X size={12} />
</button>
{/if}
</div>
{:else}
<InsertModuleButton
index={0}
placement={'bottom-center'}
on:new={(e) => {
insertFailureModule(e.detail.inlineScript)
}}
on:pickScript={(e) => {
insertFailureModule(undefined, e.detail)
}}
kind="failure"
clazz={twMerge(clazz, '!outline-none px-2 py-1.5')}
/>
{/if}
@@ -15,11 +15,8 @@
} from '$lib/components/flows/flowStateUtils.svelte'
import type { FlowModule, ScriptLang } from '$lib/gen'
import { emptyFlowModuleState } from '../utils'
import FlowSettingsItem from './FlowSettingsItem.svelte'
import FlowConstantsItem from './FlowConstantsItem.svelte'
import { dfs } from '../dfs'
import FlowErrorHandlerItem from './FlowErrorHandlerItem.svelte'
import { push } from '$lib/history'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import Portal from '$lib/components/Portal.svelte'
@@ -38,6 +35,7 @@
import { dfsByModule } from '../previousResults'
import type { InlineScript, InsertKind } from '$lib/components/graph/graphBuilder.svelte'
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
import FlowStickyNode from './FlowStickyNode.svelte'
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
interface Props {
@@ -51,6 +49,9 @@
workspace?: string | undefined
onTestUpTo?: ((id: string) => void) | undefined
onEditInput?: (moduleId: string, key: string) => void
aiChatOpen?: boolean
showFlowAiButton?: boolean
toggleAiChat?: () => void
}
let {
@@ -63,7 +64,10 @@
smallErrorHandler = false,
workspace = $workspaceStore,
onTestUpTo,
onEditInput
onEditInput,
aiChatOpen,
showFlowAiButton,
toggleAiChat
}: Props = $props()
let flowTutorials: FlowTutorials | undefined = $state(undefined)
@@ -316,14 +320,17 @@
</Portal>
<div class="flex flex-col h-full relative -pt-1">
<div
class={`z-10 sticky inline-flex flex-col gap-2 top-0 bg-surface-secondary flex-initial p-2 items-center transition-colors duration-[400ms] ease-linear border-b`}
class={`z-50 absolute inline-flex flex-col gap-2 top-3 left-1/2 -translate-x-1/2 flex-initial items-center transition-colors duration-[400ms] ease-linear bg-surface-100`}
>
{#if !disableSettings}
<FlowSettingsItem />
{/if}
{#if !disableStaticInputs}
<FlowConstantsItem />
{/if}
<FlowStickyNode
{showFlowAiButton}
{disableSettings}
{disableStaticInputs}
{smallErrorHandler}
on:generateStep
{aiChatOpen}
{toggleAiChat}
/>
</div>
<div class="z-10 flex-auto grow bg-surface-secondary" bind:clientHeight={minHeight}>
@@ -542,13 +549,6 @@
}}
/>
</div>
<div
class="z-10 absolute inline-flex w-full text-sm gap-2 bottom-0 left-0 p-2 {smallErrorHandler
? 'flex-row-reverse'
: 'justify-center'} border-b"
>
<FlowErrorHandlerItem small={smallErrorHandler} on:generateStep />
</div>
</div>
{#if !disableTutorials}
@@ -1,33 +0,0 @@
<script lang="ts">
import type { FlowEditorContext } from '../types'
import { getContext } from 'svelte'
import { classNames } from '$lib/utils'
import { Badge } from '$lib/components/common'
import { SlidersHorizontal } from 'lucide-svelte'
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
let settingsClass = $derived(
classNames(
'border w-full rounded-sm p-2 bg-surface text-sm cursor-pointer flex items-center h-[32px]',
$selectedId?.startsWith('settings')
? 'border border-1 border-slate-800 dark:bg-white/5 dark:border-slate-400/60 dark:border-gray-400'
: '',
'hover:!bg-surface-secondary active:!bg-surface'
)
)
</script>
<button onclick={() => ($selectedId = 'settings-metadata')} class={settingsClass}>
<SlidersHorizontal size={16} />
<span
class="text-xs font-bold flex flex-row justify-between w-full gap-2 items-center truncate ml-1"
>
<span>Settings</span>
<span class="h-[18px] flex items-center">
{#if flowStore.val.value.same_worker}
<Badge color="blue" baseClass="truncate">./shared</Badge>
{/if}
</span>
</span>
</button>
@@ -0,0 +1,89 @@
<script lang="ts">
import type { FlowEditorContext } from '../types'
import { getContext } from 'svelte'
import { Badge } from '$lib/components/common'
import { DollarSign, Settings } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import FlowErrorHandlerItem from './FlowErrorHandlerItem.svelte'
import FlowAIButton from '$lib/components/copilot/chat/flow/FlowAIButton.svelte'
import Popover from '$lib/components/Popover.svelte'
interface Props {
disableSettings?: boolean
disableStaticInputs?: boolean
smallErrorHandler: boolean
aiChatOpen?: boolean
showFlowAiButton?: boolean
toggleAiChat?: () => void
}
let {
disableSettings,
disableStaticInputs,
smallErrorHandler,
aiChatOpen,
showFlowAiButton,
toggleAiChat
}: Props = $props()
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
const nodeClass =
'border w-fit rounded p-1 px-2 bg-surface text-sm cursor-pointer flex items-center h-[28px] hover:!bg-surface-secondary active:!bg-surface'
const nodeSelectedClass =
'outline outline-offset-1 outline-2 outline-slate-800 dark:bg-white/5 dark:outline-slate-400/60 dark:outline-gray-400'
</script>
<div class="flex flex-row gap-2 p-1 rounded shadow-md bg-surface">
{#if !disableSettings}
<button
onclick={() => ($selectedId = 'settings-metadata')}
class={twMerge(nodeClass, $selectedId?.startsWith('settings') ? nodeSelectedClass : '')}
title="Settings"
>
<Settings size={14} />
<span
class="font-bold flex flex-row justify-between w-fit gap-2 items-center truncate ml-1.5"
>
<span class="text-xs">Settings</span>
<span class="h-[18px] flex items-center">
{#if flowStore.val.value.same_worker}
<Badge color="blue" baseClass="truncate">./shared</Badge>
{/if}
</span>
</span>
</button>
{/if}
{#if !disableStaticInputs}
<Popover>
<button
onclick={() => ($selectedId = 'constants')}
class={twMerge(nodeClass, $selectedId == 'constants' ? nodeSelectedClass : '')}
>
<DollarSign size={14} />
</button>
{#snippet text()}
Static Inputs
{/snippet}
</Popover>
{/if}
<Popover>
<FlowErrorHandlerItem small={smallErrorHandler} on:generateStep clazz={nodeClass} />
{#snippet text()}
Error Handler
{/snippet}
</Popover>
{#if showFlowAiButton}
<Popover>
<FlowAIButton
togglePanel={() => {
toggleAiChat?.()
}}
opened={aiChatOpen}
/>
{#snippet text()}
Flow AI Chat
{/snippet}
</Popover>
{/if}
</div>
@@ -2,7 +2,7 @@
import { preventDefault, stopPropagation } from 'svelte/legacy'
import { createEventDispatcher } from 'svelte'
import { Cross } from 'lucide-svelte'
import { Bug, Cross } from 'lucide-svelte'
import InsertModuleInner from './InsertModuleInner.svelte'
import { twMerge } from 'tailwind-merge'
import type { ComputeConfig } from 'svelte-floating-ui'
@@ -87,6 +87,8 @@ shouldUsePortal={true} -->
>
{#if kind === 'trigger'}
<SchedulePollIcon size={14} />
{:else if kind === 'failure'}
<Bug size={14} />
{:else}
<Cross size={iconSize} />
{/if}
@@ -5,11 +5,14 @@
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
import { enterpriseLicense } from '$lib/stores'
export let disabled: boolean = false
export let label: string
export let lang: SupportedLanguage | 'docker' | 'javascript' | undefined = undefined
interface Props {
disabled?: boolean
label: string
lang?: SupportedLanguage | 'docker' | 'javascript' | undefined
id?: string | undefined
}
export let id: string | undefined = undefined
let { disabled = false, label, lang = undefined, id = undefined }: Props = $props()
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql', 'oracledb']
</script>
@@ -32,6 +35,7 @@
<span class="text-xs">{label}</span>
</div>
</Button>
<svelte:fragment slot="text">{label} is only available with an enterprise license</svelte:fragment
>
{#snippet text()}
{label} is only available with an enterprise license
{/snippet}
</Popover>
@@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { Alert, Badge, Skeleton } from '$lib/components/common'
import { capitalize, classNames } from '$lib/utils'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
@@ -8,16 +8,21 @@
import { IntegrationService, ScriptService, type HubScriptKind } from '$lib/gen'
import { Loader2 } from 'lucide-svelte'
export let kind: HubScriptKind & string = 'script'
export let filter = ''
export let syncQuery = false
interface Props {
kind?: HubScriptKind & string
filter?: string
syncQuery?: boolean
children?: import('svelte').Snippet
}
let loading = false
let hubNotAvailable = false
let { kind = 'script', filter = $bindable(''), syncQuery = false, children }: Props = $props()
let loading = $state(false)
let hubNotAvailable = $state(false)
const dispatch = createEventDispatcher()
let appFilter: string | undefined = undefined
let appFilter: string | undefined = $state(undefined)
let items: {
path: string
summary: string
@@ -26,15 +31,12 @@
ask_id: number
app: string
kind: HubScriptKind
}[] = []
}[] = $state([])
let allApps: string[] = []
let apps: string[] = []
$: apps = filter.length > 0 ? Array.from(new Set(items?.map((x) => x.app) ?? [])).sort() : allApps
$: applyFilter(filter, kind, appFilter)
$: getAllApps(kind)
let allApps: string[] = $state([])
let apps: string[] = $derived.by(() =>
filter.length > 0 ? Array.from(new Set(items?.map((x) => x.app) ?? [])).sort() : allApps
)
async function getAllApps(filterKind: typeof kind) {
try {
@@ -71,14 +73,14 @@
limit: 40,
kind: filterKind,
app: appFilter
})
: (
})
: ((
await ScriptService.getTopHubScripts({
limit: 40,
app: appFilter,
kind: filterKind
})
).asks ?? []
).asks ?? [])
if (ts === startTs) {
loading = false
}
@@ -103,10 +105,19 @@
loading = false
}
}
$effect(() => {
;[filter, kind, appFilter]
untrack(() => applyFilter(filter, kind, appFilter))
})
$effect(() => {
kind
untrack(() => getAllApps(kind))
})
</script>
<div class="w-full flex mt-1 items-center gap-2">
<slot />
{@render children?.()}
<div class="relative w-full">
<input
type="text"
@@ -133,7 +144,7 @@
<li class="flex flex-row w-full">
<button
class="p-4 gap-4 flex flex-row grow hover:bg-surface-hover bg-surface transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
onclick={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<div
@@ -143,11 +154,8 @@
)}
>
{#if item['app'] in APP_TO_ICON_COMPONENT}
<svelte:component
this={APP_TO_ICON_COMPONENT[item['app']]}
height={18}
width={18}
/>
{@const SvelteComponent = APP_TO_ICON_COMPONENT[item['app']]}
<SvelteComponent height={18} width={18} />
{/if}
</div>
@@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { Skeleton } from '$lib/components/common'
import { classNames } from '$lib/utils'
import { APP_TO_ICON_COMPONENT } from '$lib/components/icons'
@@ -7,31 +7,40 @@
import { Circle } from 'lucide-svelte'
import Popover from '$lib/components/Popover.svelte'
export let kind: HubScriptKind & string = 'script'
export let filter = ''
export let loading = false
export let selected: number | undefined = undefined
let hubNotAvailable = false
let hubNotAvailable = $state(false)
const dispatch = createEventDispatcher()
export let appFilter: string | undefined = undefined
export let items: {
path: string
summary: string
id: number
version_id: number
ask_id: number
app: string
kind: HubScriptKind
}[] = []
export let displayPath = false
interface Props {
kind?: HubScriptKind & string
filter?: string
loading?: boolean
selected?: number | undefined
appFilter?: string | undefined
items?: {
path: string
summary: string
id: number
version_id: number
ask_id: number
app: string
kind: HubScriptKind
}[]
displayPath?: boolean
apps?: string[]
}
export let apps: string[] = []
let {
kind = 'script',
filter = $bindable(''),
loading = $bindable(false),
selected = undefined,
appFilter = undefined,
items = $bindable([]),
displayPath = false,
apps = $bindable([])
}: Props = $props()
let allApps: string[] = []
$: applyFilter(filter, kind, appFilter)
$: getAllApps(kind)
async function getAllApps(filterKind: typeof kind) {
try {
@@ -69,14 +78,14 @@
text: `${filter}`,
limit: 40,
kind: filterKind
})
: (
})
: ((
await ScriptService.getTopHubScripts({
limit: 40,
kind: filterKind,
app: appFilter
})
).asks ?? []
).asks ?? [])
const mappedItems = scripts.map(
(x: {
@@ -125,9 +134,21 @@
dispatch('pickScript', item)
}
}
$effect(() => {
;[filter, kind, appFilter]
untrack(() => {
applyFilter(filter, kind, appFilter)
})
})
$effect(() => {
kind
untrack(() => {
getAllApps(kind)
})
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
{#if hubNotAvailable}
<div class="text-2xs text-red-400 ftext-2xs font-light text-center py-2 px-3 items-center">
Hub not available
@@ -141,7 +162,7 @@
{#each items as item, index (item.path)}
<li class="w-full">
<Popover class="w-full" placement="right" forceOpen={index === selected}>
<svelte:fragment slot="text">
{#snippet text()}
<div class="flex flex-col">
<div class="text-left text-xs font-normal leading-tight py-0"
>{item.summary ?? ''}</div
@@ -150,21 +171,18 @@
{item.path ?? ''}
</div>
</div>
</svelte:fragment>
{/snippet}
<button
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
selected
? 'bg-surface-hover'
: ''}"
on:click={() => dispatch('pickScript', item)}
onclick={() => dispatch('pickScript', item)}
>
<div class={classNames('flex justify-center items-center')}>
{#if item['app'] in APP_TO_ICON_COMPONENT}
<svelte:component
this={APP_TO_ICON_COMPONENT[item['app']]}
height={14}
width={14}
/>
{@const SvelteComponent = APP_TO_ICON_COMPONENT[item['app']]}
<SvelteComponent height={14} width={14} />
{:else}
<div
class="w-[14px] h-[14px] text-gray-400 flex flex-row items-center justify-center"
@@ -1,6 +1,6 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { FlowService, ScriptService } from '$lib/gen'
import SearchItems from '$lib/components/SearchItems.svelte'
import { Skeleton } from '$lib/components/common'
@@ -9,12 +9,6 @@
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import Popover from '$lib/components/Popover.svelte'
export let kind: 'script' | 'trigger' | 'approval' | 'failure' | 'flow' | 'preprocessor' =
'script'
export let isTemplate: boolean | undefined = undefined
export let selected: number | undefined = undefined
export let displayPath = false
type Item = {
path: string
summary?: string
@@ -22,15 +16,9 @@
hash?: string
}
let items: Item[] | undefined = undefined
let items: Item[] | undefined = $state(undefined)
let filteredItems: (Item & { marked?: string })[] | undefined = undefined
export let filteredWithOwner: (Item & { marked?: string })[] | undefined = undefined
export let filter = ''
export let owners: string[] = []
$: $workspaceStore && kind && loadItems()
let filteredItems: (Item & { marked?: string })[] | undefined = $state(undefined)
async function loadItems(): Promise<void> {
items =
@@ -40,37 +28,36 @@
workspace: $workspaceStore!,
kinds: kind,
isTemplate
})
})
}
export let ownerFilter:
| { kind: 'inline' | 'owner' | 'integrations'; name: string | undefined }
| undefined = undefined
$: if ($workspaceStore) {
ownerFilter = undefined
interface Props {
kind?: 'script' | 'trigger' | 'approval' | 'failure' | 'flow' | 'preprocessor'
isTemplate?: boolean | undefined
selected?: number | undefined
displayPath?: boolean
filteredWithOwner?: (Item & { marked?: string })[] | undefined
filter?: string
owners?: string[]
ownerFilter?:
| { kind: 'inline' | 'owner' | 'integrations'; name: string | undefined }
| undefined
}
$: owners = Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort((a, b) => {
if (a.startsWith('u/') && !b.startsWith('u/')) return -1
if (b.startsWith('u/') && !a.startsWith('u/')) return 1
if (a.startsWith('f/') && !b.startsWith('f/')) return -1
if (b.startsWith('f/') && !a.startsWith('f/')) return 1
return a.localeCompare(b)
})
let {
kind = 'script',
isTemplate = undefined,
selected = undefined,
displayPath = false,
filteredWithOwner = $bindable(undefined),
filter = '',
owners = $bindable([]),
ownerFilter = $bindable(undefined)
}: Props = $props()
const dispatch = createEventDispatcher()
let lockHash = false
$: filteredWithOwner =
ownerFilter != undefined
? filteredItems?.filter((x) => x.path.startsWith(ownerFilter?.name!))
: filteredItems
function onKeyDown(e: KeyboardEvent) {
if (
selected != undefined &&
@@ -88,6 +75,33 @@
}
}
}
$effect(() => {
$workspaceStore && kind && untrack(() => loadItems())
})
$effect(() => {
if ($workspaceStore) {
ownerFilter = undefined
}
})
$effect(() => {
owners = Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort((a, b) => {
if (a.startsWith('u/') && !b.startsWith('u/')) return -1
if (b.startsWith('u/') && !a.startsWith('u/')) return 1
if (a.startsWith('f/') && !b.startsWith('f/')) return -1
if (b.startsWith('f/') && !a.startsWith('f/')) return 1
return a.localeCompare(b)
})
})
$effect(() => {
filteredWithOwner =
ownerFilter != undefined
? filteredItems?.filter((x) => x.path.startsWith(ownerFilter?.name!))
: filteredItems
})
</script>
<SearchItems
@@ -97,7 +111,7 @@
f={(x) => (emptyString(x.summary) ? x.path : x.summary + ' (' + x.path + ')')}
/>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
{#if filteredItems}
{#if filteredItems.length == 0}
<div class="text-2xs text-tertiary font-light text-center py-2 px-3 items-center">
@@ -108,20 +122,20 @@
{#each filteredWithOwner ?? [] as { path, hash, summary, marked }, index}
<li class="w-full">
<Popover class="w-full " placement="right" forceOpen={index === selected}>
<svelte:fragment slot="text">
{#snippet text()}
<div class="flex flex-col">
<div class="text-left text-xs font-normal leading-tight py-0">{summary ?? ''}</div>
<div class="text-left text-2xs font-normal">
{path ?? ''}
</div>
</div>
</svelte:fragment>
{/snippet}
<button
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
selected
? 'bg-surface-hover'
: ''}"
on:click={() => {
onclick={() => {
if (kind == 'flow') {
dispatch('pickFlow', { path: path })
} else {
@@ -234,6 +234,7 @@
boxSize = layout(dag as any)
}
const yOffset = insertable ? 100 : 0
const newNodes = dag.descendants().map((des) => ({
...des.data,
id: des.data.id,
@@ -248,7 +249,7 @@
NODE.width / 2 -
(width - fullWidth) / 2
: 0,
y: des.y || 0
y: (des.y || 0) + yOffset
}
}))
@@ -1,4 +1,4 @@
<script context="module" lang="ts">
<script module lang="ts">
export const openStore = writable('')
</script>
@@ -19,26 +19,31 @@
const POPUP_HEIGHT = 320 as const
export let id: string
let job: Job | undefined = undefined
let hovered = false
let timeout: NodeJS.Timeout | undefined
let watchJob: (id: string) => Promise<void>
let result: any
let loaded = false
let wrapper: HTMLElement
let popupOnTop = true
interface Props {
id: string
children?: import('svelte').Snippet<[any]>
}
$: open = $openStore === id
let { id, children }: Props = $props()
let job: Job | undefined = $state(undefined)
let hovered = $state(false)
let timeout: NodeJS.Timeout | undefined
let result: any = $state()
let testJobLoader: TestJobLoader | undefined = $state()
let loaded = false
let wrapper: HTMLElement | undefined = $state()
let popupOnTop = $state(true)
let open = $derived($openStore === id)
async function instantOpen() {
if (!open) {
hovered = true
popupOnTop = wrapper.getBoundingClientRect().top > POPUP_HEIGHT
popupOnTop = (wrapper?.getBoundingClientRect()?.top ?? 0) > POPUP_HEIGHT
openStore.set(id)
if (!loaded) {
await tick()
watchJob && watchJob(id)
testJobLoader?.watchJob(id)
}
} else {
timeout && clearTimeout(timeout)
@@ -81,19 +86,14 @@
})
</script>
<svelte:window on:keydown={({ key }) => ['Escape', 'Esc'].includes(key) && close()} />
<svelte:window onkeydown={({ key }) => ['Escape', 'Esc'].includes(key) && close()} />
{#if hovered}
<TestJobLoader bind:job bind:watchJob on:done={onDone} />
<TestJobLoader bind:job bind:this={testJobLoader} on:done={onDone} />
{/if}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:mouseenter={instantOpen}
on:mouseleave={staggeredClose}
bind:this={wrapper}
class="relative"
>
<slot {open} />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div onmouseenter={instantOpen} onmouseleave={staggeredClose} bind:this={wrapper} class="relative">
{@render children?.({ open })}
{#if open}
<div
transition:fade|local={{ duration: 50 }}
@@ -15,56 +15,64 @@
import WorkflowTimeline from '../WorkflowTimeline.svelte'
import Popover from '../Popover.svelte'
import { isFlowPreview, isScriptPreview, truncateRev } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { ListFilter } from 'lucide-svelte'
export let id: string
export let blankLink = false
export let workspace: string | undefined
interface Props {
id: string
blankLink?: boolean
workspace: string | undefined
}
let job: Job | undefined = undefined
let watchJob: ((id: string) => Promise<void>) | undefined = undefined
let getLogs: (() => Promise<void>) | undefined = undefined
let { id, blankLink = false, workspace }: Props = $props()
let result: any
let job: Job | undefined = $state(undefined)
let result: any = $state()
function onDone(event: { detail: Job }) {
job = event.detail
result = job['result']
}
let currentJob: Job | undefined = undefined
let currentJob: Job | undefined = $state(undefined)
$: if (currentJob?.id == id) {
job = currentJob
}
$: id && watchJob && watchJob(id)
$: job?.logs == undefined && job && viewTab == 'logs' && getLogs?.()
let lastJobId: string | undefined = undefined
let concurrencyKey: string | undefined = undefined
$: job?.id && lastJobId !== job.id && getConcurrencyKey(job)
let lastJobId: string | undefined = $state(undefined)
let concurrencyKey: string | undefined = $state(undefined)
async function getConcurrencyKey(job: Job) {
lastJobId = job.id
concurrencyKey = await ConcurrencyGroupsService.getConcurrencyKey({ id: job.id })
}
let viewTab = 'result'
let viewTab = $state('result')
function asWorkflowStatus(x: any): Record<string, WorkflowStatus> {
return x as Record<string, WorkflowStatus>
}
const dispatch = createEventDispatcher()
$effect(() => {
if (currentJob?.id == id) {
job = currentJob
}
})
$effect(() => {
id && testJobLoader && untrack(() => testJobLoader?.watchJob(id))
})
$effect(() => {
job?.logs == undefined && job && viewTab == 'logs' && untrack(() => testJobLoader?.getLogs())
})
$effect(() => {
job?.id && lastJobId !== job.id && untrack(() => job && getConcurrencyKey(job))
})
let testJobLoader: TestJobLoader | undefined = $state(undefined)
</script>
<TestJobLoader
lazyLogs
workspaceOverride={workspace}
bind:job={currentJob}
bind:getLogs
bind:watchJob
bind:this={testJobLoader}
on:done={onDone}
/>
@@ -105,7 +113,7 @@
{/if}
{#if concurrencyKey}
<Popover notClickable>
<svelte:fragment slot="text">
{#snippet text()}
This job has concurrency limits enabled with the key:
<Button
class="inline-text"
@@ -118,13 +126,13 @@
{concurrencyKey}
<ListFilter class="inline-block" size={10} />
</Button>
</svelte:fragment>
{/snippet}
<Badge large>Concurrency: {truncateRev(concurrencyKey, 20)}</Badge>
</Popover>
{/if}
{#if job?.worker}
<Popover notClickable>
<svelte:fragment slot="text">
{#snippet text()}
This job was run on worker:
<Button
class="inline-text"
@@ -134,10 +142,10 @@
dispatch('filterByWorker', job?.worker)
}}
>
{job.worker}
{job?.worker}
<ListFilter class="inline-block" size={10} />
</Button>
</svelte:fragment>
{/snippet}
<Badge large>Worker: {truncateRev(job.worker, 20)}</Badge>
</Popover>
{/if}
@@ -3,9 +3,13 @@
import { AlertTriangle } from 'lucide-svelte'
import Popover from '../Popover.svelte'
import { onDestroy } from 'svelte'
export let tag: string
interface Props {
tag: string
}
let noWorkerWithTag = false
let { tag }: Props = $props()
let noWorkerWithTag = $state(false)
let timeout: NodeJS.Timeout | undefined = undefined
@@ -39,8 +43,8 @@
{#if noWorkerWithTag}
<Popover notClickable placement="top">
<AlertTriangle size={16} class="text-yellow-500" />
<svelte:fragment slot="text">
{#snippet text()}
No worker with tag <b>{tag}</b> is currently running.
</svelte:fragment>
{/snippet}
</Popover>
{/if}
@@ -1,5 +1,5 @@
<script lang="ts">
import { onDestroy, onMount, tick } from 'svelte'
import { onDestroy, onMount, tick, untrack } from 'svelte'
import {
AppService,
FlowService,
@@ -43,12 +43,12 @@
import RunsSearch from './RunsSearch.svelte'
import AskAiButton from '../copilot/AskAiButton.svelte'
let open: boolean = false
let open: boolean = $state(false)
let searchTerm: string = ''
let textInput: HTMLInputElement
let selectedWorkspace: string | undefined = undefined
let contentSearch: ContentSearchInner | undefined = undefined
let searchTerm: string = $state('')
let textInput: HTMLInputElement | undefined = $state()
let selectedWorkspace: string | undefined = $state(undefined)
let contentSearch: ContentSearchInner | undefined = $state(undefined)
const RUNS_PREFIX = '>'
const LOGS_PREFIX = '!'
@@ -57,7 +57,7 @@
type SearchMode = 'default' | 'switch-mode' | 'runs' | 'content' | 'logs'
let tab: SearchMode = 'default'
let tab: SearchMode = $state('default')
type quickMenuItem = {
search_id: string
@@ -195,15 +195,17 @@
let defaultMenuItemsWithHidden = [...defaultMenuItems, ...hiddenMenuItems]
let itemMap = {
let itemMap = $state({
default: defaultMenuItems as any[],
'switch-mode': switchModeItems,
runs: [] as any[],
content: [] as any[],
logs: [] as any[]
}
})
$: tab === 'content' && contentSearch?.open()
$effect(() => {
tab === 'content' && contentSearch?.open()
})
async function switchPrompt(tab: string) {
if (tab === 'default') {
@@ -222,7 +224,7 @@
searchTerm = LOGS_PREFIX
}
selectedItem = selectItem(0)
textInput.focus()
textInput?.focus()
}
function removePrefix(str: string, prefix: string): string {
@@ -238,7 +240,7 @@
let defaultMenuItemLabels = defaultMenuItems.map((item) => item.label)
let defaultMenuItemAndHiddenLabels = defaultMenuItemsWithHidden.map((item) => item.label)
let switchModeItemLabels = switchModeItems.map((item) => item.label)
let askAiButton: AskAiButton | undefined
let askAiButton: AskAiButton | undefined = $state()
function fuzzyFilter(filter: string, items: any[], itemsPlainText: string[]) {
if (filter === '') {
@@ -263,7 +265,7 @@
return r
}
let queryParseErrors: string[] = []
let queryParseErrors: string[] = $state([])
async function handleSearch() {
queryParseErrors = []
@@ -332,7 +334,7 @@
return itemMap[tab][index]
}
let selectedItem: any
let selectedItem: any = $state()
async function handleKeydown(event: KeyboardEvent) {
if ((!isMac() ? event.ctrlKey : event.metaKey) && event.key === 'k') {
@@ -384,7 +386,7 @@
// Used by callbacks, call this to change the mode
function switchMode(mode: SearchMode) {
switchPrompt(mode)
textInput.focus()
textInput?.focus()
}
function gotoWindmillItemPage(e: TableAny, newtab: boolean = false) {
@@ -418,7 +420,7 @@
}
}
let mouseMoved: boolean = false
let mouseMoved: boolean = $state(false)
function handleMouseMove() {
mouseMoved = true
}
@@ -433,7 +435,10 @@
window.removeEventListener('mousemove', handleMouseMove)
})
$: searchTerm, handleSearch()
$effect(() => {
searchTerm
untrack(() => handleSearch())
})
function placeholderFromPrefix(text: string): string {
switch (text) {
@@ -470,7 +475,7 @@
type TableAny = TableScript | TableFlow | TableApp | TableRawApp
let combinedItems: TableAny[] | undefined = undefined
let combinedItems: TableAny[] | undefined = $state(undefined)
async function fetchCombinedItems() {
const scripts = await ScriptService.listScripts({
@@ -579,10 +584,10 @@
}
}
let runsSearch: RunsSearch
let runSearchRemainingCount: number | undefined = undefined
let runSearchTotalCount: number | undefined = undefined
let indexMetadata: SearchJobsIndexResponse['index_metadata'] = undefined
let runsSearch: RunsSearch | undefined = $state()
let runSearchRemainingCount: number | undefined = $state(undefined)
let runSearchTotalCount: number | undefined = $state(undefined)
let indexMetadata: SearchJobsIndexResponse['index_metadata'] = $state(undefined)
</script>
{#if open}
@@ -596,9 +601,11 @@
>
<div
class="{maxModalWidth(tab)} w-full mt-36 bg-surface rounded-lg relative"
use:clickOutside={false}
on:click_outside={() => {
open = false
use:clickOutside={{
capture: false,
onClickOutside: () => {
open = false
}
}}
>
<div class="px-4 py-2 flex flex-row gap-1 items-center border-b">
@@ -631,7 +638,7 @@
{#if queryParseErrors.length > 0}
<Popover notClickable placement="bottom-start">
<AlertTriangle size={16} class="text-yellow-500" />
<svelte:fragment slot="text">
{#snippet text()}
Some of your search terms have been ignored because one or more parse errors:<br
/><br />
<ul>
@@ -639,7 +646,7 @@
<li>- {msg}</li>
{/each}
</ul>
</svelte:fragment>
{/snippet}
</Popover>
{/if}
</div>
@@ -225,10 +225,10 @@
{#if indexMetadata?.lost_lock_ownership}
<Popover notClickable placement="top">
<AlertTriangle size={16} class="text-gray-500" />
<svelte:fragment slot="text">
{#snippet text()}
The current indexer is no longer indexing new jobs. This is most likely because of an
ongoing deployment and indexing will resume once it's complete.
</svelte:fragment>
{/snippet}
</Popover>
{/if}
</div>
@@ -266,10 +266,10 @@
{#if indexMetadata?.lost_lock_ownership}
<Popover notClickable placement="top">
<AlertTriangle size={16} class="text-gray-500" />
<svelte:fragment slot="text">
{#snippet text()}
The current indexer is no longer indexing new jobs. This is most likely because of an
ongoing deployment and indexing will resume once it's complete.
</svelte:fragment>
{/snippet}
</Popover>
{/if}
</div>
@@ -8,23 +8,45 @@
import type { MenubarMenuElements } from '@melt-ui/svelte'
import { triggerableByAI } from '$lib/actions/triggerableByAI'
export let aiId: string | undefined = undefined
export let aiDescription: string | undefined = undefined
export let label: string | undefined = undefined
export let icon: any | undefined = undefined
export let iconClasses: string | null = null
export let iconProps: any | null = null
export let isCollapsed: boolean
export let disabled: boolean = false
export let lightMode: boolean = false
export let stopPropagationOnClick: boolean = false
export let shortcut: string = ''
export let notificationsCount: number = 0
export let color: string | null = null
export let trigger: MenubarMenuElements['trigger'] | undefined = undefined
export let href: string | undefined = undefined
interface Props {
aiId?: string | undefined
aiDescription?: string | undefined
label?: string | undefined
icon?: any | undefined
iconClasses?: string | null
iconProps?: any | null
isCollapsed: boolean
disabled?: boolean
lightMode?: boolean
stopPropagationOnClick?: boolean
shortcut?: string
notificationsCount?: number
color?: string | null
trigger?: MenubarMenuElements['trigger'] | undefined
href?: string | undefined
class?: string | undefined
}
let buttonRef: HTMLButtonElement | undefined = undefined
let {
aiId = undefined,
aiDescription = undefined,
label = undefined,
icon = undefined,
iconClasses = null,
iconProps = null,
isCollapsed,
disabled = false,
lightMode = false,
stopPropagationOnClick = false,
shortcut = '',
notificationsCount = 0,
color = null,
trigger = undefined,
href = undefined,
class: classNames = undefined
}: Props = $props()
let buttonRef: HTMLButtonElement | undefined = $state(undefined)
let dispatch = createEventDispatcher()
</script>
@@ -48,76 +70,74 @@
}
}
}}
on:click={(e) => {
if (stopPropagationOnClick) e.preventDefault()
if (href) {
goto(href)
}
dispatch('click')
}}
class={twMerge(
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
lightMode
? 'text-primary data-[highlighted]:bg-surface-hover hover:bg-surface-hover'
: 'data-[highlighted]:bg-[#2A3648] hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
color ? 'border-4' : '',
'transition-all relative',
$$props.class
)}
style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''}
use:conditionalMelt={trigger}
title={isCollapsed ? undefined : label}
{...$trigger}
>
{#if icon}
<svelte:component
this={icon}
size={16}
class={twMerge(
'flex-shrink-0',
lightMode
? 'text-primary group-hover:text-secondary'
: 'text-primary-inverse group-hover:text-secondary-inverse dark:group-hover:text-secondary dark:text-primary',
'transition-all',
iconClasses
)}
{...iconProps}
/>
{/if}
onclick={(e) => {
if (stopPropagationOnClick) e.preventDefault()
if (href) {
goto(href)
}
dispatch('click')
}}
class={twMerge(
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
lightMode
? 'text-primary data-[highlighted]:bg-surface-hover hover:bg-surface-hover'
: 'data-[highlighted]:bg-[#2A3648] hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
color ? 'border-4' : '',
'transition-all relative',
classNames
)}
style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''}
use:conditionalMelt={trigger}
title={isCollapsed ? undefined : label}
{...$trigger}
>
{#if icon}
{@const SvelteComponent = icon}
<SvelteComponent
size={16}
class={twMerge(
'flex-shrink-0',
lightMode
? 'text-primary group-hover:text-secondary'
: 'text-primary-inverse group-hover:text-secondary-inverse dark:group-hover:text-secondary dark:text-primary',
'transition-all',
iconClasses
)}
{...iconProps}
/>
{/if}
{#if !isCollapsed && label}
<span
class={twMerge(
'whitespace-pre truncate',
lightMode ? 'text-primary' : 'text-primary-inverse dark:text-primary',
'transition-all',
$$props.class
)}
>
{label}
<span
class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold"
>
{shortcut}
</span>
{#if !isCollapsed && label}
<span
class={twMerge(
'whitespace-pre truncate',
lightMode ? 'text-primary' : 'text-primary-inverse dark:text-primary',
'transition-all',
classNames
)}
>
{label}
<span class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold">
{shortcut}
</span>
{/if}
</span>
{/if}
{#if isCollapsed && notificationsCount > 0}
<div class="absolute translate-x-1/2 translate-y-1/2 -top-2 right-1 flex h-fit w-fit">
<SideBarNotification notificationCount={notificationsCount} small={true} />
</div>
{:else if notificationsCount > 0}
<div class="ml-auto">
<SideBarNotification notificationCount={notificationsCount} small={false} />
</div>
{/if}
{#if isCollapsed && notificationsCount > 0}
<div class="absolute translate-x-1/2 translate-y-1/2 -top-2 right-1 flex h-fit w-fit">
<SideBarNotification notificationCount={notificationsCount} small={true} />
</div>
{:else if notificationsCount > 0}
<div class="ml-auto">
<SideBarNotification notificationCount={notificationsCount} small={false} />
</div>
{/if}
</button>
<svelte:fragment slot="text">
{#snippet text()}
{#if label}
{label}
{/if}
</svelte:fragment>
{/snippet}
</Popover>
{/if}
@@ -1,23 +1,40 @@
<script lang="ts">
import { classNames, conditionalMelt } from '$lib/utils'
import { conditionalMelt } from '$lib/utils'
import type { MenubarMenuElements } from '@melt-ui/svelte'
import { navigating, page } from '$app/stores'
import Popover from '../Popover.svelte'
import { base } from '$app/paths'
import { triggerableByAI } from '$lib/actions/triggerableByAI'
import { goto } from '$app/navigation'
import { twMerge } from 'tailwind-merge'
export let aiId: string | undefined = undefined
export let aiDescription: string | undefined = undefined
export let label: string
export let href: string
export let icon: any | undefined = undefined
export let isCollapsed: boolean
export let disabled: boolean = false
export let lightMode: boolean = false
export let item: MenubarMenuElements['item'] | undefined = undefined
interface Props {
aiId?: string | undefined
aiDescription?: string | undefined
label: string
href: string
icon?: any | undefined
isCollapsed: boolean
disabled?: boolean
lightMode?: boolean
item?: MenubarMenuElements['item'] | undefined
class?: string
}
let isSelected = false
let {
aiId = undefined,
aiDescription = undefined,
label,
href,
icon = undefined,
isCollapsed,
disabled = false,
lightMode = false,
item = undefined,
class: classNames = ''
}: Props = $props()
let isSelected = $state(false)
navigating.subscribe(() => {
if (href === `${base}/`) {
@@ -39,62 +56,62 @@
goto(href)
}
}}
class={classNames(
'group flex items-center px-2 py-2 text-sm font-light rounded-md h-8 gap-3',
isSelected
? lightMode
? 'bg-surface-selected hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
: 'bg-frost-700 hover:bg-[#30404e] data-[highlighted]:bg-[#30404e]'
: lightMode
? 'hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
: 'hover:bg-[#2A3648] data-[highlighted]:bg-[#2A3648]',
class={twMerge(
'group flex items-center px-2 py-2 text-sm font-light rounded-md h-8 gap-3',
isSelected
? lightMode
? 'bg-surface-selected hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
: 'bg-frost-700 hover:bg-[#30404e] data-[highlighted]:bg-[#30404e]'
: lightMode
? 'hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
: 'hover:bg-[#2A3648] data-[highlighted]:bg-[#2A3648]',
'hover:transition-all',
$$props.class
)}
target={href.includes('http') ? '_blank' : null}
title={isCollapsed ? undefined : label}
use:conditionalMelt={item}
{...$item}
>
{#if icon}
<svelte:component
this={icon}
size={16}
class={classNames(
'flex-shrink-0',
isSelected
? lightMode
? 'text-primary group-hover:text-secondary'
: 'text-frost-200 group-hover:text-white'
: lightMode
? 'text-primary group-hover:text-secondary'
: 'text-gray-100 group-hover:text-white',
'transition-all'
)}
/>
{/if}
'hover:transition-all',
classNames
)}
target={href.includes('http') ? '_blank' : null}
title={isCollapsed ? undefined : label}
use:conditionalMelt={item}
{...$item}
>
{#if icon}
{@const SvelteComponent = icon}
<SvelteComponent
size={16}
class={twMerge(
'flex-shrink-0',
isSelected
? lightMode
? 'text-primary group-hover:text-secondary'
: 'text-frost-200 group-hover:text-white'
: lightMode
? 'text-primary group-hover:text-secondary'
: 'text-gray-100 group-hover:text-white',
'transition-all'
)}
/>
{/if}
{#if !isCollapsed}
<span
class={classNames(
'whitespace-pre truncate',
isSelected
? lightMode
? 'text-primary group-hover:text-secondary'
: 'text-frost-200 group-hover:text-white'
: lightMode
? 'text-primary group-hover:text-secondary'
: 'text-gray-100 group-hover:text-white',
'transition-all duration-75'
)}
>
{label}
</span>
{/if}
{#if !isCollapsed}
<span
class={twMerge(
'whitespace-pre truncate',
isSelected
? lightMode
? 'text-primary group-hover:text-secondary'
: 'text-frost-200 group-hover:text-white'
: lightMode
? 'text-primary group-hover:text-secondary'
: 'text-gray-100 group-hover:text-white',
'transition-all duration-75'
)}
>
{label}
</span>
{/if}
</a>
<svelte:fragment slot="text">
{#snippet text()}
{label}
</svelte:fragment>
{/snippet}
</Popover>
{/if}
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { ArrowDown, ArrowUp, Download, MoreVertical, MoveVertical, Columns } from 'lucide-svelte'
import Dropdown from '../DropdownV2.svelte'
import Cell from './Cell.svelte'
@@ -17,19 +19,21 @@
import Popover from '../Popover.svelte'
import DarkModeObserver from '../DarkModeObserver.svelte'
import DownloadCsv from './DownloadCsv.svelte'
export let objects: Array<Record<string, any>> = []
interface Props {
objects?: Array<Record<string, any>>
}
let currentPage = 1
let perPage = 25
let search: string = ''
let { objects = [] }: Props = $props()
let currentPage = $state(1)
let perPage = $state(25)
let search: string = $state('')
let structuredObjects: {
_id: number
rowData: Record<string, any>
}[] = []
let headers: string[] = []
$: recomputeObjectsAndHeaders(objects)
}[] = $state([])
let headers: string[] = $state([])
function recomputeObjectsAndHeaders(objects: Array<Record<string, any>>) {
;[headers, structuredObjects] = computeStructuredObjectsAndHeaders(objects)
@@ -43,16 +47,12 @@
}
}
$: perPage && adjustCurrentPage()
$: data = computeData(structuredObjects, activeSorting, search)
type ActiveSorting = {
column: string
direction: 'asc' | 'desc'
}
let activeSorting: ActiveSorting | undefined = undefined
let activeSorting: ActiveSorting | undefined = $state(undefined)
function sortObjects(
activeSorting: ActiveSorting | undefined,
@@ -99,10 +99,8 @@
return sortObjects(activeSorting, objects, true)
}
$: slicedData = data.slice((currentPage - 1) * perPage, currentPage * perPage)
let selection = [] as Array<number>
let colSelection = [] as Array<string>
let selection = $state([] as Array<number>)
let colSelection = $state([] as Array<string>)
// Function to handle individual row checkbox change
function handleCheckboxChange(rowId: number) {
@@ -132,7 +130,7 @@
}
}
let renderCount = 0
let renderCount = $state(0)
const badgeColors: BadgeColor[] = ['gray', 'blue', 'green', 'yellow', 'indigo']
const darkBadgeColors: BadgeColor[] = [
@@ -142,8 +140,8 @@
'dark-yellow',
'dark-indigo'
]
let darkMode = false
let wrapperWidth = 0
let darkMode = $state(false)
let wrapperWidth = $state(0)
// function isSortable(key: string) {
// let value = objects?.[0]?.[key]
@@ -160,6 +158,14 @@
colSelection = [...colSelection, key]
}
}
run(() => {
recomputeObjectsAndHeaders(objects)
})
run(() => {
perPage && adjustCurrentPage()
})
let data = $derived(computeData(structuredObjects, activeSorting, search))
let slicedData = $derived(data.slice((currentPage - 1) * perPage, currentPage * perPage))
</script>
<DarkModeObserver bind:darkMode />
@@ -242,12 +248,12 @@
return actions
}}
>
<svelte:fragment slot="buttonReplacement">
{#snippet buttonReplacement()}
<MoreVertical
size={8}
class="w-8 h-8 p-2 hover:bg-surface-hover cursor-pointer rounded-md"
/>
</svelte:fragment>
{/snippet}
</Dropdown>
</div>
</div>
@@ -281,7 +287,7 @@
<Head>
<tr>
<Cell head first={true} last={false}>
<input type="checkbox" class="!w-4 !h-4" on:change={handleSelectAllChange} />
<input type="checkbox" class="!w-4 !h-4" onchange={handleSelectAllChange} />
</Cell>
{#each headers ?? [] as key, index}
<Cell head last={index == headers.length - 1}>
@@ -290,7 +296,7 @@
{#if activeSorting?.column === key}
<button
class="p-1 w-6 h-6 flex justify-center items-center"
on:click={() => {
onclick={() => {
activeSorting = {
column: key,
direction: activeSorting?.direction == 'asc' ? 'desc' : 'asc'
@@ -306,7 +312,7 @@
{:else}
<button
class="p-1 w-6 h-6 flex justify-center items-center"
on:click={() => {
onclick={() => {
activeSorting = {
column: key,
direction: activeSorting?.direction == 'asc' ? 'desc' : 'asc'
@@ -320,7 +326,7 @@
type="checkbox"
class="!w-4 !h-4"
checked={colSelection.includes(key)}
on:change={() => handleColumnSelected(key)}
onchange={() => handleColumnSelected(key)}
/>
</div>
</Cell>
@@ -335,7 +341,7 @@
type="checkbox"
class="!w-4 !h-4"
checked={selection.includes(_id)}
on:change={() => handleCheckboxChange(_id)}
onchange={() => handleCheckboxChange(_id)}
/>
</Cell>
{#each headers as key, index}
@@ -404,7 +410,9 @@
>
{txt?.length > 100 ? txt.slice(0, 100) + '...' : txt}
</div>
<svelte:fragment slot="text">{txt}</svelte:fragment>
{#snippet text()}
{txt}
{/snippet}
</Popover>
{/if}
</Cell>
@@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import Label from '../Label.svelte'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
@@ -16,21 +16,32 @@
type: 'bar' | 'scatter' | 'line' | 'area' | 'range-bar' | 'range-area'
}
let component: GridItem | undefined = undefined
let component = $state(undefined) as GridItem | undefined
$: if (component === undefined && $selectedComponent && $app) {
component = findGridItem($app, $selectedComponent[0])
$effect(() => {
if (component === undefined && $selectedComponent && $app) {
untrack(() => {
component = findGridItem($app, $selectedComponent[0])
})
}
})
let isEE = $derived(component?.data?.type === 'agchartscomponentee')
interface Props {
value?: Dataset | undefined
trigger?: import('svelte').Snippet
}
$: isEE = component?.data.type === 'agchartscomponentee'
export let value: Dataset | undefined = undefined
let { value = $bindable(undefined), trigger }: Props = $props()
const dispatch = createEventDispatcher()
function removeDataset() {
dispatch('remove')
}
const trigger_render = $derived(trigger)
</script>
<Popover
@@ -41,10 +52,10 @@
}}
closeOnOtherPopoverOpen
>
<svelte:fragment slot="trigger">
<slot name="trigger" />
</svelte:fragment>
<svelte:fragment slot="content">
{#snippet trigger()}
{@render trigger_render?.()}
{/snippet}
{#snippet content()}
{#if value}
<div class="flex flex-col w-96 gap-4 p-4 max-h-[70vh] overflow-y-auto">
<Label label="Name">
@@ -66,5 +77,5 @@
<Button color="red" size="xs" on:click={removeDataset}>Remove dataset</Button>
</div>
{/if}
</svelte:fragment>
{/snippet}
</Popover>
@@ -29,7 +29,12 @@
cellRendererType: 'text' | 'badge' | 'link'
}
export let value: Column | undefined
interface Props {
value: Column | undefined
trigger?: import('svelte').Snippet
}
let { value = $bindable(), trigger: trigger_render }: Props = $props()
const presets = [
{
@@ -82,11 +87,13 @@
}
]
let renderCount = 0
let renderCount = $state(0)
$: if (value && value.cellRendererType === null) {
value.cellRendererType = 'text'
}
$effect(() => {
if (value && value.cellRendererType === null) {
value.cellRendererType = 'text'
}
})
</script>
<Popover
@@ -97,10 +104,10 @@
}}
closeOnOtherPopoverOpen
>
<svelte:fragment slot="trigger">
<slot name="trigger" />
</svelte:fragment>
<svelte:fragment slot="content">
{#snippet trigger()}
{@render trigger_render?.()}
{/snippet}
{#snippet content()}
{#if value}
<div class="flex flex-col w-96 p-4 gap-4 max-h-[70vh] overflow-y-auto">
<span class="text-sm mb-2 leading-6 font-semibold">
@@ -132,7 +139,7 @@
</Label>
<Label label="Flex">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.ag-grid.com/javascript-data-grid/column-sizing/#column-flex"
>
@@ -146,7 +153,7 @@
The column with flex: 2 has twice the size with flex: 1. So final sizes will be:
150px, 100px, 200px.
</Tooltip>
</svelte:fragment>
{/snippet}
<input type="range" step="1" bind:value={value.flex} min={1} max={12} />
<div class="text-xs">{value.flex}</div>
@@ -164,7 +171,7 @@
</Label>
<Label label="Value formatter">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.ag-grid.com/javascript-data-grid/value-formatters/"
>
@@ -172,8 +179,8 @@
one type (e.g. numeric) but needs to be converted for human reading (e.g. putting in
currency symbols and number formatting).
</Tooltip>
</svelte:fragment>
<svelte:fragment slot="action">
{/snippet}
{#snippet action()}
<Button
size="xs"
color="light"
@@ -186,21 +193,20 @@
>
Clear
</Button>
</svelte:fragment>
{/snippet}
</Label>
<div>
{#key renderCount}
<div class="flex flex-col gap-4">
<div class="relative">
{#if !presets.find((preset) => preset.value === value?.valueFormatter)}
<div
class="z-50 absolute bg-opacity-50 bg-surface top-0 left-0 bottom-0 right-0"
<div class="z-50 absolute bg-opacity-50 bg-surface top-0 left-0 bottom-0 right-0"
></div>
{/if}
<div class="text-xs font-semibold">Presets</div>
<select
bind:value={value.valueFormatter}
on:change={() => {
onchange={() => {
renderCount++
}}
placeholder="Code"
@@ -231,12 +237,12 @@
</Label>
<Label label="Filter">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip documentationLink="https://www.ag-grid.com/javascript-data-grid/filtering/">
Filtering allows you to limit the rows displayed in your grid to those that match
criteria you specify.
</Tooltip>
</svelte:fragment>
{/snippet}
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
@@ -248,36 +254,36 @@
</Label>
<!--
EE only
EE only
<Label label="Aggregation function">
<SimpleEditor autoHeight lang="javascript" bind:code={value.aggFunc} />
</Label>
<Label label="Aggregation function">
<SimpleEditor autoHeight lang="javascript" bind:code={value.aggFunc} />
</Label>
<Label label="Pivot">
<Toggle bind:checked={value.pivot} size="xs" />
</Label>
<Label label="Pivot">
<Toggle bind:checked={value.pivot} size="xs" />
</Label>
<Label label="Pivot index">
<input type="number" placeholder="pivot index" bind:value={value.pivotIndex} />
</Label>
<Label label="Pivot index">
<input type="number" placeholder="pivot index" bind:value={value.pivotIndex} />
</Label>
<Label label="Pinned">
<select bind:value={value.pinned}>
<option value={null}>None</option>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</Label>
<Label label="Pinned">
<select bind:value={value.pinned}>
<option value={null}>None</option>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</Label>
<Label label="Row group">
<Toggle bind:checked={value.rowGroup} size="xs" />
</Label>
<Label label="Row group">
<Toggle bind:checked={value.rowGroup} size="xs" />
</Label>
<Label label="Row group index">
<input type="number" placeholder="row group index" bind:value={value.rowGroupIndex} />
</Label>
-->
<Label label="Row group index">
<input type="number" placeholder="row group index" bind:value={value.rowGroupIndex} />
</Label>
-->
<Label label="Type">
<select bind:value={value.cellRendererType}>
@@ -302,5 +308,5 @@
{/if}
</div>
{/if}
</svelte:fragment>
{/snippet}
</Popover>
@@ -13,7 +13,12 @@
name: string
}
export let value: Dataset | undefined = undefined
interface Props {
value?: Dataset | undefined
trigger?: import('svelte').Snippet
}
let { value = $bindable(undefined), trigger: trigger_render }: Props = $props()
const dispatch = createEventDispatcher()
@@ -30,10 +35,10 @@
}}
closeOnOtherPopoverOpen
>
<svelte:fragment slot="trigger">
<slot name="trigger" />
</svelte:fragment>
<svelte:fragment slot="content">
{#snippet trigger()}
{@render trigger_render?.()}
{/snippet}
{#snippet content()}
{#if value}
<div class="flex flex-col w-96 p-4 gap-4 max-h-[70vh] overflow-y-auto">
<Label label="Name">
@@ -61,5 +66,5 @@
<Button color="red" size="xs" on:click={removeDataset}>Remove dataset</Button>
</div>
{/if}
</svelte:fragment>
{/snippet}
</Popover>
@@ -11,9 +11,13 @@
import { ColumnIdentity, type ColumnDef } from '../apps/components/display/dbtable/utils'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
export let value: ColumnDef | undefined
import Alert from '../common/alert/Alert.svelte'
interface Props {
value: ColumnDef | undefined
trigger?: import('svelte').Snippet
}
let { value = $bindable(), trigger: trigger_render }: Props = $props()
const presets = [
{
@@ -71,7 +75,7 @@
}
]
let renderCount = 0
let renderCount = $state(0)
function computeWarning(columnMetadata, value) {
if (columnMetadata?.isnullable === 'NO' && !columnMetadata?.defaultvalue) {
@@ -118,7 +122,7 @@
return null
}
$: warning = computeWarning(value, value)
let warning = $derived(computeWarning(value, value))
</script>
<Popover
@@ -130,27 +134,27 @@
contentClasses="max-h-[70vh] overflow-y-auto p-4 flex flex-col gap-4 w-96"
closeOnOtherPopoverOpen
>
<svelte:fragment slot="trigger">
<slot name="trigger" />
</svelte:fragment>
{#snippet trigger()}
{@render trigger_render?.()}
{/snippet}
<svelte:fragment slot="content">
{#snippet content()}
{#if value}
<Section label="Column settings">
<svelte:fragment slot="header">
{#snippet header()}
<Badge color="blue">
{value.field}
</Badge>
</svelte:fragment>
{/snippet}
<Label label="Skip for select and update">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
By default, all columns are included in the select and update queries. If you want to
exclude a column from the select and update queries, you can set this property to
true.
</Tooltip>
</svelte:fragment>
<svelte:fragment slot="action">
{/snippet}
{#snippet action()}
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
@@ -159,7 +163,7 @@
size="xs"
disabled={value?.isprimarykey}
/>
</svelte:fragment>
{/snippet}
{#if value?.isprimarykey}
<Alert type="warning" size="xs" title="Primary key" class="my-1">
You cannot skip a primary key.
@@ -168,14 +172,14 @@
</Label>
<Label label="Hide from insert">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
By default, all columns are used to generate the submit form. If you want to exclude a
column from the submit form, you can set this property to true. If the column is not
nullable or doesn't have a default value, a default value will be required.
</Tooltip>
</svelte:fragment>
<svelte:fragment slot="action">
{/snippet}
{#snippet action()}
<Toggle
disabled={value?.isidentity === ColumnIdentity.Always}
on:pointerdown={(e) => {
@@ -184,7 +188,7 @@
bind:checked={value.hideInsert}
size="xs"
/>
</svelte:fragment>
{/snippet}
</Label>
{#if value?.isidentity === ColumnIdentity.Always}
<Alert type="warning" size="xs" title="Identity column" class="my-1">
@@ -216,13 +220,13 @@
/>
{/if}
<Label label="Default input">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
By default, all columns are used to generate the submit form. If you want to exclude a
column from the submit form, you can set this property to true. If the column is not
nullable or doesn't have a default value, a default value will be required.
</Tooltip>
</svelte:fragment>
{/snippet}
{#if value?.datatype}
{@const type = value?.datatype}
@@ -266,7 +270,7 @@
<Section label="AG Grid configuration">
<div
class={twMerge('flex flex-col gap-4', value.ignored ? 'opacity-50 cursor-none ' : '')}
on:pointerdown={(e) => {
onpointerdown={(e) => {
if (value?.ignored) {
e?.stopPropagation()
}
@@ -292,7 +296,7 @@
</Label>
<Label label="Flex">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.ag-grid.com/javascript-data-grid/column-sizing/#column-flex"
>
@@ -306,7 +310,7 @@
remaining. The column with flex: 2 has twice the size with flex: 1. So final sizes
will be: 150px, 100px, 200px.
</Tooltip>
</svelte:fragment>
{/snippet}
<input type="range" step="1" bind:value={value.flex} min={1} max={12} />
<div class="text-xs">{value.flex}</div>
@@ -324,7 +328,7 @@
</Label>
<Label label="Value formatter">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.ag-grid.com/javascript-data-grid/value-formatters/"
>
@@ -332,8 +336,8 @@
one type (e.g. numeric) but needs to be converted for human reading (e.g. putting in
currency symbols and number formatting).
</Tooltip>
</svelte:fragment>
<svelte:fragment slot="action">
{/snippet}
{#snippet action()}
<Button
size="xs"
color="light"
@@ -346,7 +350,7 @@
>
Clear
</Button>
</svelte:fragment>
{/snippet}
</Label>
<div>
{#key renderCount}
@@ -360,7 +364,7 @@
<div class="text-xs font-semibold">Presets</div>
<select
bind:value={value.valueFormatter}
on:change={() => {
onchange={() => {
renderCount++
}}
placeholder="Code"
@@ -392,5 +396,5 @@
</div>
</Section>
{/if}
</svelte:fragment>
{/snippet}
</Popover>
@@ -11,7 +11,7 @@
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
let closeOnOutsideClick = true
let closeOnOutsideClick = $state(true)
type Dataset = {
value: RichConfiguration
@@ -23,7 +23,12 @@
extraOptions?: { mode: 'markers' | 'lines' | 'lines+markers' } | undefined
}
export let value: Dataset | undefined = undefined
interface Props {
value?: Dataset | undefined
trigger?: import('svelte').Snippet
}
let { value = $bindable(undefined), trigger: trigger_render }: Props = $props()
const dispatch = createEventDispatcher()
@@ -40,10 +45,10 @@
}}
{closeOnOutsideClick}
>
<svelte:fragment slot="trigger">
<slot name="trigger" />
</svelte:fragment>
<svelte:fragment slot="content">
{#snippet trigger()}
{@render trigger_render?.()}
{/snippet}
{#snippet content()}
{#if value}
<div class="flex flex-col w-96 p-4 gap-4 max-h-[70vh] overflow-y-auto">
<Label label="Name">
@@ -55,7 +60,7 @@
<option value="bar">Bar</option>
<option
value="scatter"
on:click={() => {
onclick={() => {
if (value && value?.extraOptions === undefined) {
value.extraOptions = { mode: 'markers' }
}
@@ -77,13 +82,13 @@
{/if}
<Label label="Aggregation method">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
A method to aggregate the data. For example, if you have multiple x data points with
the same value, you can choose to sum them up or take the mean. If you don't have
multiple x data points with the same value, this option will have no effect.
</Tooltip>
</svelte:fragment>
{/snippet}
<select bind:value={value.aggregation_method}>
<option value="sum">Sum</option>
<option value="mean">Mean</option>
@@ -128,5 +133,5 @@
<Button color="red" size="xs" on:click={removeDataset}>Remove dataset</Button>
</div>
{/if}
</svelte:fragment>
{/snippet}
</Popover>
@@ -5,11 +5,18 @@
import Toggle from '../Toggle.svelte'
import Tooltip from '../Tooltip.svelte'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
export let column: {
headerName: string
hideColumn: boolean
type: 'text' | 'badge' | 'link'
interface Props {
column: {
headerName: string
hideColumn: boolean
type: 'text' | 'badge' | 'link'
}
trigger?: import('svelte').Snippet
}
let { column = $bindable(), trigger }: Props = $props()
const trigger_render = $derived(trigger)
</script>
<Popover
@@ -21,10 +28,10 @@
closeButton
closeOnOtherPopoverOpen
>
<svelte:fragment slot="trigger">
<slot name="trigger" />
</svelte:fragment>
<svelte:fragment slot="content">
{#snippet trigger()}
{@render trigger_render?.()}
{/snippet}
{#snippet content()}
<div class="flex flex-col w-96 p-4 gap-4">
<span class="text-sm mb-2 leading-6 font-semibold">
Table Column
@@ -73,5 +80,5 @@
</Alert>
{/if}
</div>
</svelte:fragment>
{/snippet}
</Popover>
@@ -17,12 +17,13 @@
import { Pen, Trash, Plus } from 'lucide-svelte'
import Head from '$lib/components/table/Head.svelte'
import Row from '$lib/components/table/Row.svelte'
import { untrack } from 'svelte'
type FolderW = Folder & { canWrite: boolean }
let newFolderName: string = ''
let folders: FolderW[] | undefined = undefined
let folderDrawer: Drawer
let newFolderName: string = $state('')
let folders: FolderW[] | undefined = $state(undefined)
let folderDrawer: Drawer | undefined = $state()
async function loadFolders(): Promise<void> {
folders = (await FolderService.listFolders({ workspace: $workspaceStore! })).map((x) => {
@@ -53,16 +54,18 @@
$userStore?.folders.push(newFolderName)
loadFolders()
editFolderName = newFolderName
folderDrawer.openDrawer()
folderDrawer?.openDrawer()
}
$: {
$effect(() => {
if ($workspaceStore && $userStore) {
loadFolders()
untrack(() => {
loadFolders()
})
}
}
})
let editFolderName: string = ''
let editFolderName: string = $state('')
function computeMembers(owners: string[], extra_perms: Record<string, any>) {
const members = new Set(owners)
@@ -96,13 +99,13 @@
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
contentClasses="flex flex-col gap-2 p-4"
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button size="md" startIcon={{ icon: Plus }} nonCaptureEvent>New folder</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<input
class="mr-2"
on:keyup={(e) => handleKeyUp(e, () => close())}
onkeyup={(e) => handleKeyUp(e, () => close())}
placeholder="New folder name"
bind:value={newFolderName}
/>
@@ -120,7 +123,7 @@
Create
</Button>
</div>
</svelte:fragment>
{/snippet}
</Popover>
</div>
</PageHeader>
@@ -161,7 +164,7 @@
hoverable
on:click={() => {
editFolderName = name
folderDrawer.openDrawer()
folderDrawer?.openDrawer()
}}
>
<Cell first>
@@ -183,7 +186,7 @@
disabled: !canWrite,
action: () => {
editFolderName = name
folderDrawer.openDrawer()
folderDrawer?.openDrawer()
}
},
{
@@ -111,7 +111,6 @@
let testJobLoader: TestJobLoader | undefined = $state(undefined)
let persistentScriptDrawer: PersistentScriptDrawer | undefined = $state(undefined)
let getLogs: (() => Promise<void>) | undefined = $state(undefined)
let showExplicitProgressTip: boolean = $state(
(localStorage.getItem('hideExplicitProgressTip') ?? 'false') == 'false'
@@ -352,7 +351,11 @@
}
}
$effect(() => {
job?.logs == undefined && job && viewTab == 'logs' && isNotFlow(job?.job_kind) && getLogs?.()
job?.logs == undefined &&
job &&
viewTab == 'logs' &&
isNotFlow(job?.job_kind) &&
testJobLoader?.getLogs()
})
$effect(() => {
job?.id && lastJobId !== job.id && untrack(() => getConcurrencyKey(job))
@@ -415,7 +418,6 @@
bind:scriptProgress
on:done={() => job?.['result'] != undefined && (viewTab = 'result')}
bind:this={testJobLoader}
bind:getLogs
bind:isLoading={testIsLoading}
bind:job
bind:jobUpdateLastFetch
@@ -1207,7 +1207,6 @@
<Tooltip
light
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs"
scale={0.9}
wrapperClass="flex items-center"
>
All past and schedule executions of scripts and flows, including previews. You only see
@@ -7,8 +7,8 @@
import { devopsRole } from '$lib/stores'
import { Search, AlertTriangle } from 'lucide-svelte'
let searchTerm = $page.url.searchParams.get('query') ?? ''
let queryParseErrors: string[] | undefined = undefined
let searchTerm = $state($page.url.searchParams.get('query') ?? '')
let queryParseErrors: string[] | undefined = $state(undefined)
</script>
<div class="flex flex-col w-full h-screen max-h-screen max-w-screen px-2">
@@ -40,15 +40,17 @@
{#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0}
<Popover notClickable placement="bottom-start">
<AlertTriangle size={16} class="text-yellow-500" />
<svelte:fragment slot="text">
{#snippet text()}
Some of your search terms have been ignored because one or more parse errors:<br /><br
/>
<ul>
{#each queryParseErrors as msg}
<li>- {msg}</li>
{/each}
</ul>
</svelte:fragment>
{#if queryParseErrors}
<ul>
{#each queryParseErrors as msg}
<li>- {msg}</li>
{/each}
</ul>
{/if}
{/snippet}
</Popover>
{/if}
</div>
@@ -37,37 +37,43 @@
EyeOff,
Circle
} from 'lucide-svelte'
import { untrack } from 'svelte'
type ListableVariableW = ListableVariable & { canWrite: boolean }
let filter = ''
let variables: ListableVariableW[] | undefined = undefined
let filteredItems: (ListableVariableW & { marked?: string })[] | undefined = undefined
let contextualVariables: ContextualVariable[] = []
let shareModal: ShareModal
let variableEditor: VariableEditor
let contextualVariableEditor: ContextualVariableEditor
let loading = {
let filter = $state('')
let variables = $state(undefined) as ListableVariableW[] | undefined
let filteredItems = $state(undefined) as (ListableVariableW & { marked?: string })[] | undefined
let contextualVariables: ContextualVariable[] = $state([])
let shareModal: ShareModal | undefined = $state()
let variableEditor: VariableEditor | undefined = $state()
let contextualVariableEditor: ContextualVariableEditor | undefined = $state()
let loading = $state({
contextual: true
}
})
let deleteConfirmedCallback: (() => void) | undefined = undefined
$: open = Boolean(deleteConfirmedCallback)
let deleteConfirmedCallback: (() => void) | undefined = $state(undefined)
let open = $derived(Boolean(deleteConfirmedCallback))
$: owners = Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
let owners = $derived(
Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
)
let ownerFilter: string | undefined = undefined
let ownerFilter: string | undefined = $state(undefined)
$: if ($workspaceStore) {
ownerFilter = undefined
}
$effect(() => {
if ($workspaceStore) {
ownerFilter = undefined
}
})
$: preFilteredItems =
let preFilteredItems = $derived(
ownerFilter == undefined
? variables
: variables?.filter((x) => x.path.startsWith(ownerFilter ?? ''))
)
// If relative, the dropdown is positioned relative to its button
async function loadVariables(): Promise<void> {
@@ -79,7 +85,7 @@
})
}
let deployUiSettings: WorkspaceDeployUISettings | undefined = undefined
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
async function getDeployUiSettings() {
if (!$enterpriseLicense) {
@@ -107,15 +113,17 @@
sendUserToast(`Variable ${path} was deleted`)
}
$: {
$effect(() => {
if ($workspaceStore && $userStore) {
loadVariables()
loadContextualVariables()
untrack(() => {
loadVariables()
loadContextualVariables()
})
}
}
let tab: 'workspace' | 'contextual' = 'workspace'
})
let tab: 'workspace' | 'contextual' = $state('workspace')
let deploymentDrawer: DeployWorkspaceDrawer
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
async function deleteContextualVariable(row: { name: string }) {
await WorkspaceService.setEnvironmentVariable({
@@ -161,12 +169,12 @@
<Button
size="md"
startIcon={{ icon: Plus }}
on:click={() => contextualVariableEditor.initNew()}
on:click={() => contextualVariableEditor?.initNew()}
>
New&nbsp;contextual&nbsp;variable
</Button>
{:else}
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor.initNew()}>
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor?.initNew()}>
New&nbsp;variable
</Button>
{/if}
@@ -247,7 +255,7 @@
<a
class="break-all"
id="edit-{path}"
on:click={() => variableEditor.editVariable(path)}
onclick={() => variableEditor?.editVariable(path)}
href="#{path}"
>
{#if marked}
@@ -269,7 +277,9 @@
{#if is_secret}
<Popover notClickable>
<EyeOff size={12} />
<span slot="text">This item is secret</span>
{#snippet text()}
<span>This item is secret</span>
{/snippet}
</Popover>
{/if}
</span>
@@ -283,19 +293,23 @@
{#if is_linked}
<Popover notClickable>
<Link size={16} />
<div slot="text">
This variable is linked with a resource of the same path. They are
deleted and renamed together.
</div>
{#snippet text()}
<div>
This variable is linked with a resource of the same path. They are
deleted and renamed together.
</div>
{/snippet}
</Popover>
{/if}
{#if account}
<Popover notClickable>
<RefreshCw size={16} />
<div slot="text">
This OAuth token will be kept up-to-date in the background by Windmill
using its refresh token
</div>
{#snippet text()}
<div>
This OAuth token will be kept up-to-date in the background by Windmill
using its refresh token
</div>
{/snippet}
</Popover>
{/if}
@@ -314,9 +328,11 @@
/>
</div>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
{#snippet text()}
<div>
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
{/snippet}
</Popover>
{:else if is_expired}
<Popover notClickable>
@@ -324,11 +340,13 @@
class="text-yellow-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
The access_token is expired, it will get renewed the next time this
variable is fetched or you can request is to be refreshed in the
dropdown on the right.
</div>
{#snippet text()}
<div>
The access_token is expired, it will get renewed the next time
this variable is fetched or you can request is to be refreshed in
the dropdown on the right.
</div>
{/snippet}
</Popover>
{:else}
<Popover notClickable>
@@ -336,10 +354,12 @@
class="text-green-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
The variable was connected through OAuth and the token is not
expired.
</div>
{#snippet text()}
<div>
The variable was connected through OAuth and the token is not
expired.
</div>
{/snippet}
</Popover>
{/if}
</div>
@@ -354,7 +374,7 @@
{
displayName: 'Edit',
icon: Pen,
action: () => variableEditor.editVariable(path),
action: () => variableEditor?.editVariable(path),
disabled: !canWrite
},
{
@@ -382,15 +402,15 @@
displayName: 'Deploy to prod/staging',
icon: FileUp,
action: () => {
deploymentDrawer.openDrawer(path, 'variable')
deploymentDrawer?.openDrawer(path, 'variable')
}
}
]
]
: []),
{
displayName: owner ? 'Share' : 'See Permissions',
action: () => {
shareModal.openDrawer(path, 'variable')
shareModal?.openDrawer(path, 'variable')
},
icon: Share
},
@@ -411,7 +431,7 @@
loadVariables()
}
}
]
]
: [])
]
}}
@@ -446,7 +466,7 @@
return [
{
displayName: 'Edit',
action: () => contextualVariableEditor.editVariable(row.name, row.value)
action: () => contextualVariableEditor?.editVariable(row.name, row.value)
},
{
displayName: 'Delete',
@@ -456,7 +476,7 @@
}
}
]
}
}
: undefined}
/>
{/if}