mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 <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
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
})}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
+79
-48
@@ -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={{
|
||||
|
||||
+12
-12
@@ -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>
|
||||
|
||||
+8
-4
@@ -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)}
|
||||
|
||||
@@ -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 contextual variable
|
||||
</Button>
|
||||
{:else}
|
||||
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor.initNew()}>
|
||||
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor?.initNew()}>
|
||||
New 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}
|
||||
|
||||
Reference in New Issue
Block a user