feat: better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0

This commit is contained in:
Ruben Fiszel
2025-06-12 15:44:35 +02:00
committed by GitHub
parent d47c078bb5
commit 23920aee84
310 changed files with 15289 additions and 11442 deletions
+26 -20
View File
@@ -24,9 +24,10 @@
"@redocly/json-to-json-schema": "^0.0.1",
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^0.1.15",
"@xyflow/svelte": "^1.0.0",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -3076,11 +3077,11 @@
"integrity": "sha512-VqAAkydywPpkw63WQhPVKCD3SdwXuihCUVZbbiY3SfSTGQyHmwRoq27y4dmJdZuJwd5JIlQoMPyGvMbUPY0RKQ=="
},
"node_modules/@svelte-put/shortcut": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.1.1.tgz",
"integrity": "sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-4.1.0.tgz",
"integrity": "sha512-wImNEIkbxAIWFqlfuhcbC+jRPDeRa/uJGIXHMEVVD+jqL9xCwWNnkGQJ6Qb2XVszuRLHlb8SGZDL3Io/h3vs8w==",
"peerDependencies": {
"svelte": "^3.55.0 || ^4.0.0 || ^5.0.0"
"svelte": "^5.1.0"
}
},
"node_modules/@sveltejs/acorn-typescript": {
@@ -3274,6 +3275,17 @@
"node": ">=10.13.0"
}
},
"node_modules/@tutorlatin/svelte-tiny-virtual-list": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.2.tgz",
"integrity": "sha512-Zts/GUrU6aPgmCrL6sGiD1EoiQxUU+r7Q2g9pckEUIDybNkluV2pEYWUsQLQ+28IJlSSHTopv0fhKs3qMUxqEw==",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -3871,22 +3883,21 @@
"peer": true
},
"node_modules/@xyflow/svelte": {
"version": "0.1.39",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.39.tgz",
"integrity": "sha512-QZ5mzNysvJeJW7DxmqI4Urhhef9tclqtPr7WAS5zQF5Gk6k9INwzey4CYNtEZo8XMj9H8lzgoJRmgMPnJEc1kw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-1.0.2.tgz",
"integrity": "sha512-biNnzWRmKF/m7t7iY9wt3/KhoTyA8XGbS0Vix3HCL9vrULDF14R1VsMIOxKZib1GJfG+f1/cD2dQ9kplfPLs/Q==",
"dependencies": {
"@svelte-put/shortcut": "3.1.1",
"@xyflow/system": "0.0.59",
"classcat": "^5.0.4"
"@svelte-put/shortcut": "^4.1.0",
"@xyflow/system": "0.0.61"
},
"peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
"svelte": "^5.25.0"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.59",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.59.tgz",
"integrity": "sha512-+xgqYhoBv5F10TQx0SiKZR/DcWtuxFYR+e/LluHb7DMtX4SsMDutZWEJ4da4fDco25jZxw5G9fOlmk7MWvYd5Q==",
"version": "0.0.61",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.61.tgz",
"integrity": "sha512-TsZG/Ez8dzxX6/Ol44LvFqVZsYvyz6dpDlAQZZk6hTL7JLGO5vN3dboRJqMwU8/Qtr5IEv5YBzojjAwIqW1HCA==",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-selection": "^3.0.10",
@@ -4710,11 +4721,6 @@
"consola": "^3.2.3"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+2 -1
View File
@@ -94,8 +94,8 @@
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@xyflow/svelte": "^1.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^0.1.15",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.3.4",
@@ -135,6 +135,7 @@
"svelte-exmarkdown": "^5.0.0",
"svelte-infinite-loading": "^1.4.0",
"svelte-tiny-virtual-list": "^2.0.5",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"tailwind-merge": "^1.13.2",
"vscode": "npm:@codingame/monaco-vscode-extension-api@~16.1.1",
"vscode-languageclient": "~9.0.1",
+35 -33
View File
@@ -13,8 +13,8 @@
const dispatch = createEventDispatcher()
let email: string
let username: string
let email: string | undefined = $state()
let username: string | undefined = $state()
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
@@ -24,7 +24,7 @@
}
}
let automateUsernameCreation = false
let automateUsernameCreation = $state(false)
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? false
@@ -35,17 +35,17 @@
await WorkspaceService.addUser({
workspace: $workspaceStore!,
requestBody: {
email,
email: email!,
username: automateUsernameCreation ? undefined : username,
is_admin: selected == 'admin',
operator: selected == 'operator'
}
})
sendUserToast(`Added ${email}`)
if (!(await UserService.existsEmail({ email }))) {
if (!(await UserService.existsEmail({ email: email! }))) {
let isSuperadmin = $superadmin
if (!isCloudHosted()) {
const emailCopy = email
const emailCopy = email!
sendUserToast(
`User ${email} is not registered yet on the instance. ${
!isSuperadmin
@@ -62,7 +62,7 @@
goto('#superadmin-settings')
}
}
]
]
: []
)
}
@@ -70,47 +70,49 @@
dispatch('new')
}
let selected: 'operator' | 'developer' | 'admin' = 'developer'
let selected: 'operator' | 'developer' | 'admin' = $state('developer')
</script>
<Popover placement="bottom-end">
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: UserPlus }}>
Add new user
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<div class="flex flex-col w-72 p-4">
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
<span class="text-xs mb-1 leading-6">Email</span>
<input type="email mb-1" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
<input type="email mb-1" onkeyup={handleKeyUp} placeholder="email" bind:value={email} />
{#if !automateUsernameCreation}
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
<input type="text" onkeyup={handleKeyUp} placeholder="username" bind:value={username} />
{/if}
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4" let:item>
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
<ToggleButtonGroup bind:selected class="mb-4">
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
{/snippet}
</ToggleButtonGroup>
<Button
variant="contained"
@@ -129,5 +131,5 @@
Add
</Button>
</div>
</svelte:fragment>
{/snippet}
</Popover>
@@ -23,31 +23,43 @@
import Popover from './meltComponents/Popover.svelte'
import Button from './common/button/Button.svelte'
import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte'
import { onDestroy } from 'svelte'
import { onDestroy, untrack } from 'svelte'
export let resourceType: string
export let resourceTypeInfo: ResourceType | undefined
export let args: Record<string, any> | any = {}
export let linkedSecret: string | undefined = undefined
export let isValid = true
export let linkedSecretCandidates: string[] | undefined = undefined
export let description: string | undefined = undefined
interface Props {
resourceType: string
resourceTypeInfo: ResourceType | undefined
args?: Record<string, any> | any
linkedSecret?: string | undefined
isValid?: boolean
linkedSecretCandidates?: string[] | undefined
description?: string | undefined
}
let schema = emptySchema()
let notFound = false
let {
resourceType,
resourceTypeInfo,
args = $bindable({}),
linkedSecret = $bindable(undefined),
isValid = $bindable(true),
linkedSecretCandidates = undefined,
description = $bindable(undefined)
}: Props = $props()
let supabaseWizard = false
let schema = $state(emptySchema())
let notFound = $state(false)
let loadingGithubInstallations = false
let githubInstallations: GetGlobalConnectedRepositoriesResponse = []
let workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse = []
let selectedGHAppAccountId: string | undefined = undefined
let selectedGHAppRepository: string | undefined = undefined
let githubInstallationUrl: string | undefined = undefined
let supabaseWizard = $state(false)
let loadingGithubInstallations = $state(false)
let githubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse = $state([])
let selectedGHAppAccountId: string | undefined = $state(undefined)
let selectedGHAppRepository: string | undefined = $state(undefined)
let githubInstallationUrl: string | undefined = $state(undefined)
let installationCheckInterval: number | undefined = undefined
let isCheckingInstallation = false
let importJwt = ''
let githubAppPopover: { open: () => void; close: () => void } | null = null
let isCheckingInstallation = $state(false)
let importJwt = $state('')
let githubAppPopover: { open: () => void; close: () => void } | null = $state(null)
async function loadGithubInstallations() {
if (!$enterpriseLicense) return
@@ -159,9 +171,6 @@
notFound = true
}
}
$: $workspaceStore && loadSchema()
$: notFound && rawCode && parseJson()
function parseJson() {
try {
@@ -173,13 +182,9 @@
error = e.message
}
}
let error = ''
let rawCode = ''
let viewJsonSchema = false
$: rawCode && parseJson()
$: textFileContent && parseTextFileContent()
let error = $state('')
let rawCode = $state('')
let viewJsonSchema = $state(false)
function switchTab(asJson: boolean) {
viewJsonSchema = asJson
@@ -193,11 +198,8 @@
}
}
$: resourceType == 'postgresql' && isSupabaseAvailable()
$: resourceType == 'git_repository' && $userStore?.is_admin && loadGithubInstallations()
let connectionString = ''
let validConnectionString = true
let connectionString = $state('')
let validConnectionString = $state(true)
function parseConnectionString(close: (_: any) => void) {
const regex =
/postgres(?:ql)?:\/\/(?<user>[^:@]+)(?::(?<password>[^@]+))?@(?<host>[^:\/?]+)(?::(?<port>\d+))?\/(?<dbname>[^\?]+)?(?:\?.*sslmode=(?<sslmode>[^&]+))?/
@@ -241,8 +243,8 @@
close(null)
}
let rawCodeEditor: SimpleEditor | undefined = undefined
let textFileContent: string
let rawCodeEditor: SimpleEditor | undefined = $state(undefined)
let textFileContent: string | undefined = $state(undefined)
function parseTextFileContent() {
args = {
@@ -250,13 +252,6 @@
}
}
$: githubInstallationsNotInWorkspace = githubInstallations.filter((installation) => {
return !workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
})
async function deleteInstallation(installation_id: number) {
if (!$workspaceStore) {
sendUserToast('Failed to delete installation', true)
@@ -329,6 +324,34 @@
}
}
}
$effect(() => {
$workspaceStore && untrack(() => loadSchema())
})
$effect(() => {
notFound && rawCode && untrack(() => parseJson())
})
$effect(() => {
rawCode && untrack(() => parseJson())
})
$effect(() => {
textFileContent && untrack(() => parseTextFileContent())
})
$effect(() => {
resourceType == 'postgresql' && untrack(() => isSupabaseAvailable())
})
$effect(() => {
resourceType == 'git_repository' &&
$userStore?.is_admin &&
untrack(() => loadGithubInstallations())
})
let githubInstallationsNotInWorkspace = $derived(
githubInstallations.filter((installation) => {
return !workspaceGithubInstallations.some(
(workspaceInstallation) =>
workspaceInstallation.installation_id === installation.installation_id
)
})
)
</script>
{#if !notFound}
@@ -346,7 +369,7 @@
placement: 'bottom'
}}
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
spacingSize="sm"
size="xs"
@@ -357,8 +380,8 @@
>
From connection string
</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="w-[550px] flex flex-col items-start gap-1">
<div class="flex flex-row gap-1 w-full">
@@ -384,7 +407,7 @@
{/if}
</div>
</div>
</svelte:fragment>
{/snippet}
</Popover>
{/if}
{#if resourceType == 'postgresql' && supabaseWizard}
@@ -419,7 +442,7 @@
}}
disabled={!$enterpriseLicense || loadingGithubInstallations}
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
color="none"
variant="border"
@@ -433,8 +456,8 @@
>
{$enterpriseLicense ? 'GitHub App' : 'GitHub App (ee only)'}
</Button>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<div class="block text-primary p-4">
<div class="flex flex-col gap-4 w-[600px]">
{#if workspaceGithubInstallations.length > 0}
@@ -654,7 +677,7 @@
</div>
</div>
</div>
</svelte:fragment>
{/snippet}
</Popover>
{:else}
<Button
@@ -61,29 +61,31 @@
on:refresh
express={expressOAuthSetup}
/>
<div slot="actions" class="flex gap-1">
{#if step > 1}
<Button variant="border" on:click={appConnectInner?.back}>Back</Button>
{/if}
{#if isGoogleSignin}
<button {disabled} on:click={appConnectInner?.next}>
<img
class="h-10 w-auto object-contain"
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
alt="Google sign-in"
/>
</button>
{:else}
<Button {disabled} on:click={appConnectInner?.next}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
Next
{:else}
Save
{/if}
</Button>
{/if}
</div>
{#snippet actions()}
<div class="flex gap-1">
{#if step > 1}
<Button variant="border" on:click={appConnectInner?.back ?? (() => {})}>Back</Button>
{/if}
{#if isGoogleSignin}
<button {disabled} on:click={appConnectInner?.next}>
<img
class="h-10 w-auto object-contain"
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
alt="Google sign-in"
/>
</button>
{:else}
<Button {disabled} on:click={appConnectInner?.next ?? (() => {})}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
Next
{:else}
Save
{/if}
</Button>
{/if}
</div>
{/snippet}
</DrawerContent>
</Drawer>
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { userStore, workspaceStore } from '$lib/stores'
import IconedResourceType from './IconedResourceType.svelte'
import {
@@ -27,14 +29,25 @@
import { apiTokenApps, forceSecretValue, linkedSecretValue } from './app_connect'
import type { SchemaProperty } from '$lib/common'
export let step = 1
export let resourceType = ''
export let isGoogleSignin = false
export let disabled = false
export let manual = true
export let express = false
interface Props {
step?: number
resourceType?: string
isGoogleSignin?: boolean
disabled?: boolean
manual?: boolean
express?: boolean
}
let isValid = true
let {
step = $bindable(1),
resourceType = $bindable(''),
isGoogleSignin = $bindable(false),
disabled = $bindable(false),
manual = $bindable(true),
express = false
}: Props = $props()
let isValid = $state(true)
const nativeLanguagesCategory = [
'postgresql',
@@ -46,15 +59,15 @@
'oracledb'
]
let filter = ''
let value: string = ''
let filter = $state('')
let value: string = $state('')
let valueToken: TokenResponse | undefined = undefined
let connects: string[] | undefined = undefined
let connects: string[] | undefined = $state(undefined)
let connectsManual:
| [string, { img?: string; instructions: string[]; key?: string }][]
| undefined = undefined
let args: any = {}
let renderDescription = true
| undefined = $state(undefined)
let args: any = $state({})
let renderDescription = $state(true)
function computeCandidates(resourceType: string, argsKeys: string[], passwords: string[]) {
return apiTokenApps[resourceType]?.linkedSecret
@@ -66,8 +79,8 @@
)
}
let linkedSecret: string | undefined = undefined
let linkedSecretCandidates: string[] | undefined = undefined
let linkedSecret: string | undefined = $state(undefined)
let linkedSecretCandidates: string[] | undefined = $state(undefined)
function computeLinkedSecret(resourceType: string, argsKeys: string[], passwords: string[]) {
linkedSecretCandidates = computeCandidates(resourceType, argsKeys, passwords)
return (
@@ -76,14 +89,14 @@
)
}
let scopes: string[] = []
let scopes: string[] = $state([])
let extra_params: [string, string][] = []
let path: string
let description = ''
let path: string = $state('')
let description = $state('')
let resourceTypeInfo: ResourceType | undefined = undefined
let resourceTypeInfo: ResourceType | undefined = $state(undefined)
let pathError = ''
let pathError = $state('')
export async function open(rt?: string) {
if (!rt) {
@@ -112,7 +125,9 @@
async function loadConnects() {
if (!connects) {
try {
connects = (await OauthService.listOauthConnects()).filter((x) => x != 'supabase_wizard')
connects = (await OauthService.listOauthConnects())
.filter((x) => x != 'supabase_wizard')
.sort((a, b) => a.localeCompare(b))
} catch (e) {
connects = []
console.error('Error loading OAuth connects', e)
@@ -122,27 +137,31 @@
const connectAndManual = ['gitlab']
$: isGoogleSignin =
step == 1 &&
(resourceType == 'google' ||
resourceType == 'gmail' ||
resourceType == 'gcal' ||
resourceType == 'gdrive' ||
resourceType == 'gsheets')
run(() => {
isGoogleSignin =
step == 1 &&
(resourceType == 'google' ||
resourceType == 'gmail' ||
resourceType == 'gcal' ||
resourceType == 'gdrive' ||
resourceType == 'gsheets')
})
$: disabled =
(step == 1 && resourceType == '') ||
(step == 2 &&
value == '' &&
args &&
args['token'] == '' &&
args['password'] == '' &&
args['api_key'] == '' &&
args['key'] == '' &&
linkedSecret != undefined) ||
step == 3 ||
(step == 4 && pathError != '') ||
!isValid
run(() => {
disabled =
(step == 1 && resourceType == '') ||
(step == 2 &&
value == '' &&
args &&
args['token'] == '' &&
args['password'] == '' &&
args['api_key'] == '' &&
args['key'] == '' &&
linkedSecret != undefined) ||
step == 3 ||
(step == 4 && pathError != '') ||
!isValid
})
export async function loadResourceTypes() {
if (connectsManual) {
@@ -154,14 +173,18 @@
connectsManual = availableRts
.filter((x) => connectAndManual.includes(x) || !Object.keys(connects ?? {}).includes(x))
.map((x) => [
x,
apiTokenApps[x] ?? {
instructions: '',
img: undefined,
linkedSecret: undefined
}
])
.map(
(x) =>
[
x,
apiTokenApps[x] ?? {
instructions: '',
img: undefined,
linkedSecret: undefined
}
] as [string, { img?: string; instructions: string[]; key?: string }]
)
.sort((a, b) => a[0].localeCompare(b[0]))
const filteredNativeLanguages = filteredConnectsManual?.filter(
(o) => nativeLanguagesCategory?.includes(o[0]) ?? false
)
@@ -177,7 +200,7 @@
}
function popupListener(event) {
console.log('Received oauth popup message')
console.log('Received oauth popup message', event)
let data = event.data
if (event.origin == null || event.origin !== window.location.origin) {
console.log(
@@ -280,6 +303,9 @@
// dispatch('close')
// }
} else {
if (!path) {
throw Error('Path is not set')
}
let exists = await VariableService.existsVariable({
workspace: $workspaceStore!,
path
@@ -370,31 +396,29 @@
}
}
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{ error: string; refresh: string; close: void }>()
let filteredConnects: { key: string }[] = []
let filteredConnects: { key: string }[] = $state([])
let filteredConnectsManual: [string, { img?: string; instructions: string[]; key?: string }][] =
[]
$state([])
let editScopes = false
let editScopes = $state(false)
</script>
{#if !express}
<SearchItems
{filter}
items={connects
? connects
.sort((a, b) => a.localeCompare(b))
.map((key) => ({
key
}))
? connects.map((key) => ({
key
}))
: undefined}
bind:filteredItems={filteredConnects}
f={(x) => x.key}
/>
<SearchItems
{filter}
items={connectsManual?.sort((a, b) => a[0].localeCompare(b[0]))}
items={connectsManual}
bind:filteredItems={filteredConnectsManual}
f={(x) => x[0]}
/>
@@ -590,7 +614,7 @@
</div>
<h3 class="mb-4 flex gap-4"
>Scopes <button
on:click={() => {
onclick={() => {
editScopes = !editScopes
}}><Pen size={14} /></button
></h3
+50 -43
View File
@@ -1,60 +1,67 @@
<script lang="ts">
import type { EnumType } from '$lib/common'
import AutoComplete from 'simple-svelte-autocomplete'
import { createEventDispatcher } from 'svelte'
import { twMerge } from 'tailwind-merge'
import Select from './Select.svelte'
export let disabled: boolean
export let value: any
export let enum_: EnumType
export let autofocus: boolean | null
export let defaultValue: string | undefined
export let valid: boolean
export let create: boolean
export let required: boolean
export let enumLabels: Record<string, string> | undefined = undefined
interface Props {
disabled: boolean
value: string | undefined
enum_: EnumType
autofocus: boolean | null
defaultValue: string | undefined
valid: boolean
create: boolean
enumLabels?: Record<string, string> | undefined
}
let {
disabled,
value = $bindable(),
enum_,
autofocus,
defaultValue,
valid,
create,
enumLabels = undefined
}: Props = $props()
const dispatch = createEventDispatcher()
let customItems: string[] = []
let customItems: string[] = $state([])
function onCreate(newItem: string) {
console.log(newItem)
customItems = [...customItems, newItem]
let items = $derived.by(() => {
const l = [...(enum_ ? enum_ : []), ...customItems].map((item) => ({
value: item,
label: enumLabels?.[item] ?? item
}))
if (create && filterText && l.every((i) => i.value !== filterText)) {
l.push({ value: filterText, label: `Add new: ${filterText}` })
}
return l
})
return newItem
}
let filterText = $state('')
</script>
<div class="w-full flex-col">
<div class="w-full">
<AutoComplete
items={[...(required ? [] : ['']), ...(enum_ ? enum_ : []), ...customItems]}
labelFunction={(val) => (enumLabels ? enumLabels[val] ?? val : val)}
bind:selectedItem={value}
inputClassName={twMerge(
'bg-surface-secondary flex',
valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'
)}
value={value ?? defaultValue}
hideArrow={true}
dropdownClassName="!text-sm !py-2 !rounded-sm !border-gray-200 !border !shadow-md !bg-surface-primary"
className="w-full"
noInputStyles
onFocus={() => {
dispatch('focus')
}}
onBlur={() => {
dispatch('blur')
}}
{create}
{required}
{onCreate}
<Select
inputClass={valid ? '' : '!border-red-500/60'}
clearable
{disabled}
{autofocus}
createText="Press enter to use this non-predefined value"
autofocus={autofocus ?? undefined}
bind:filterText
{items}
bind:value={
() => value ?? defaultValue,
(newValue) => {
if (newValue && items.findIndex((i) => i.value === newValue) === -1)
customItems.push(newValue)
value = newValue
}
}
onFocus={() => dispatch('focus')}
onBlur={() => dispatch('blur')}
/>
</div>
</div>
+2 -2
View File
@@ -33,7 +33,7 @@
<Drawer bind:this={jsonViewer} size="800px">
<DrawerContent title="Argument Details" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
on:click={() => copyToClipboard(JSON.stringify(jsonViewerContent, null, 4))}
color="light"
@@ -42,7 +42,7 @@
>
Copy
</Button>
</svelte:fragment>
{/snippet}
{#if isString(jsonViewerContent)}
<pre>{jsonViewerContent}</pre>
{:else}
+296 -189
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { preventDefault, stopPropagation, createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import type { EnumType, SchemaProperty } from '$lib/common'
import {
setInputCat as computeInputCat,
@@ -7,7 +10,7 @@
getSchemaFromProperties
} from '$lib/utils'
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
import { createEventDispatcher, onMount, tick } from 'svelte'
import { createEventDispatcher, onMount, tick, untrack } from 'svelte'
import Multiselect from 'svelte-multiselect'
import { fade } from 'svelte/transition'
import { Button, SecondsInput } from './common'
@@ -36,99 +39,167 @@
import { deepEqual } from 'fast-equals'
import DynSelect from './DynSelect.svelte'
import type { Script } from '$lib/gen'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { ComponentCustomCSS } from './apps/types'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let label: string = ''
export let value: any
export let defaultValue: any = undefined
interface Props {
label?: string
value: any
defaultValue?: any
description?: string
format?: string
contentEncoding?: 'base64' | 'binary' | undefined
type?: string | undefined
oneOf?: SchemaProperty[] | undefined
required?: boolean
pattern?: undefined | string
valid?: boolean
enum_?: EnumType
disabled?: boolean
itemsType?:
| {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
multiselect?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
| undefined
displayHeader?: boolean
properties?: { [name: string]: SchemaProperty } | undefined
nestedRequired?: string[] | undefined
autofocus?: boolean | null
compact?: boolean
password?: boolean
pickForField?: string | undefined
variableEditor?: VariableEditor | undefined
itemPicker?: ItemPicker | undefined
noMargin?: boolean
extra?: Record<string, any>
minW?: boolean
prettifyHeader?: boolean
resourceTypes: string[] | undefined
disablePortal?: boolean
showSchemaExplorer?: boolean
simpleTooltip?: string | undefined
customErrorMessage?: string | undefined
onlyMaskPassword?: boolean
nullable?: boolean
title?: string | undefined
placeholder?: string | undefined
order?: string[] | undefined
editor?: SimpleEditor | undefined
orderEditable?: boolean
shouldDispatchChanges?: boolean
noDefaultOnSelectFirst?: boolean
helperScript?:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined
otherArgs?: Record<string, any>
lightHeader?: boolean
diffStatus?: SchemaDiff | undefined
hideNested?: boolean
nestedParent?: { label: string; nestedParent: any | undefined } | undefined
nestedClasses?: string
displayType?: boolean
css?: ComponentCustomCSS<'schemaformcomponent'> | undefined
appPath?: string | undefined
computeS3ForceViewerPolicies?:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined
workspace?: string | undefined
actions?: import('svelte').Snippet
}
export let description: string = ''
export let format: string = ''
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
export let type: string | undefined = undefined
export let oneOf: SchemaProperty[] | undefined = undefined
export let required = false
export let pattern: undefined | string = undefined
export let valid = true
export let enum_: EnumType = undefined
export let disabled = false
export let itemsType:
| {
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
contentEncoding?: 'base64'
enum?: string[]
multiselect?: string[]
resourceType?: string
properties?: { [name: string]: SchemaProperty }
}
| undefined = undefined
let {
label = '',
value = $bindable(),
defaultValue = $bindable(undefined),
description = $bindable(undefined),
format = $bindable(undefined),
contentEncoding = undefined,
type = undefined,
oneOf = $bindable(undefined),
required = false,
pattern = $bindable(undefined),
valid = $bindable(undefined),
enum_ = $bindable(undefined),
disabled = false,
itemsType = $bindable(undefined),
displayHeader = true,
properties = $bindable(undefined),
nestedRequired = undefined,
autofocus = null,
compact = false,
password = false,
pickForField = $bindable(undefined),
variableEditor = undefined,
itemPicker = undefined,
noMargin = false,
extra = {},
minW = true,
prettifyHeader = false,
resourceTypes,
disablePortal = false,
showSchemaExplorer = false,
simpleTooltip = undefined,
customErrorMessage = undefined,
onlyMaskPassword = false,
nullable = false,
title = $bindable(undefined),
placeholder = $bindable(undefined),
order = $bindable(undefined),
editor = $bindable(undefined),
orderEditable = false,
shouldDispatchChanges = false,
noDefaultOnSelectFirst = false,
helperScript = undefined,
otherArgs = {},
lightHeader = false,
diffStatus = undefined,
hideNested = false,
nestedParent = undefined,
nestedClasses = '',
displayType = true,
css = undefined,
appPath = undefined,
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
actions
}: Props = $props()
export let displayHeader = true
export let properties: { [name: string]: SchemaProperty } | undefined = undefined
export let nestedRequired: string[] | undefined = undefined
export let autofocus: boolean | null = null
export let compact = false
export let password = false
export let pickForField: string | undefined = undefined
export let variableEditor: VariableEditor | undefined = undefined
export let itemPicker: ItemPicker | undefined = undefined
export let noMargin = false
export let extra: Record<string, any> = {}
export let minW = true
export let prettifyHeader = false
export let resourceTypes: string[] | undefined
export let disablePortal = false
export let showSchemaExplorer = false
export let simpleTooltip: string | undefined = undefined
export let customErrorMessage: string | undefined = undefined
export let onlyMaskPassword = false
export let nullable: boolean = false
export let title: string | undefined = undefined
export let placeholder: string | undefined = undefined
export let order: string[] | undefined = undefined
export let editor: SimpleEditor | undefined = undefined
export let orderEditable = false
export let shouldDispatchChanges: boolean = false
export let noDefaultOnSelectFirst: boolean = false
export let helperScript:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined = undefined
export let otherArgs: Record<string, any> = {}
export let lightHeader = false
export let diffStatus: SchemaDiff | undefined = undefined
export let hideNested = false
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
export let nestedClasses = ''
export let displayType = true
export let css: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
export let appPath: string | undefined = undefined
export let computeS3ForceViewerPolicies:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined = undefined
export let workspace: string | undefined = undefined
$effect(() => {
if (description == undefined) {
description = ''
}
if (format == undefined) {
format = ''
}
if (valid == undefined) {
valid = true
}
})
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
let oneOfSelected: string | undefined = undefined
let oneOfSelected: string | undefined = $state(undefined)
let tagKey = $derived(
oneOf?.find((o) => Object.keys(o.properties ?? {})?.includes('kind')) ? 'kind' : 'label'
)
async function updateOneOfSelected(oneOf: SchemaProperty[] | undefined) {
if (
oneOf &&
oneOf.length >= 2 &&
(!oneOfSelected || !oneOf.some((o) => o.title === oneOfSelected) || !value)
) {
const tagKey = oneOf.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
if (value && value[tagKey] && oneOf.some((o) => o.title === value[tagKey])) {
const existingValue = JSON.parse(JSON.stringify(value))
oneOfSelected = value[tagKey]
@@ -142,10 +213,6 @@
}
}
$: updateOneOfSelected(oneOf)
$: oneOf && value && onOneOfChange()
function onOneOfChange() {
const label = value?.['label']
const kind = value?.['kind']
@@ -159,51 +226,48 @@
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
let ignoreValueUndefined = false
let error: string = ''
let s3FilePicker: S3FilePicker
let s3FileUploadRawMode: false
let isListJson = false
let hasIsListJsonChanged = false
let ignoreValueUndefined = $state(false)
let error: string = $state('')
let s3FilePicker: S3FilePicker | undefined = $state()
let s3FileUploadRawMode: boolean = $state(false)
let isListJson = $state(false)
let hasIsListJsonChanged = $state(false)
let el: HTMLTextAreaElement | undefined = undefined
let el: HTMLTextAreaElement | undefined = $state(undefined)
let rawValue: string | undefined = undefined
let rawValue: string | undefined = $state(undefined)
function computeDefaultValue(
nvalue?: any,
inputCat?: string,
defaultValue?: any,
nnullable?: boolean
) {
function computeDefaultValue(inputCat?: string, defaultValue?: any, nnullable?: boolean) {
let nvalue: any = null
if (label == 'toString' && typeof value == 'function') {
value = undefined
nvalue = undefined
}
if ((value == undefined || value == null) && !ignoreValueUndefined) {
value = structuredClone(defaultValue)
nvalue = structuredClone($state.snapshot(defaultValue))
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
value = nullable ? null : ''
nvalue = nullable ? null : ''
} else if (inputCat == 'enum' && required) {
value = enum_?.[0]
nvalue = enum_?.[0]
} else if (inputCat == 'boolean') {
value = false
nvalue = false
} else if (inputCat == 'list') {
value = []
nvalue = []
}
} else if (inputCat === 'object') {
evalValueToRaw()
}
}
if (nnullable && type === 'string' && value === '') {
if (nnullable && type === 'string' && value === '' && nvalue != null) {
value = null
} else if (nvalue != null && !deepEqual(nvalue, value)) {
console.log('set value 2', label, nvalue, value)
value = nvalue
}
}
$: computeDefaultValue(value, inputCat, defaultValue, nullable)
let lastValue: any = undefined
let lastValue: any = $state(undefined)
// By setting isListJson to true, we can render inputs even if the value is not an array of the correct type
// This avoids the issue of the input being rendered as a string with value: [object Object], or as a number with value: NaN
@@ -233,16 +297,7 @@
lastValue = value
}
$: !isListJson &&
inputCat === 'list' &&
value != lastValue &&
itemsType?.type &&
!hasIsListJsonChanged &&
checkArrayValueType()
$: defaultValue != undefined && handleDefaultValueChange()
let oldDefaultValue = structuredClone(defaultValue)
let oldDefaultValue = structuredClone($state.snapshot(defaultValue))
function handleDefaultValueChange() {
if (
deepEqual(value, oldDefaultValue) &&
@@ -251,7 +306,7 @@
) {
value = defaultValue
}
oldDefaultValue = structuredClone(defaultValue)
oldDefaultValue = structuredClone($state.snapshot(defaultValue))
}
function isObjectCat(inputCat?: string) {
@@ -286,11 +341,6 @@
}
let setCodeDisabled = false
$: (inputCat &&
(isObjectCat(inputCat) || isRawStringEditor(inputCat)) &&
!oneOf &&
evalValueToRaw()) ||
value
let timeout: NodeJS.Timeout | undefined = undefined
function setNewValueFromCode(nvalue: any) {
@@ -389,10 +439,8 @@
e.stopPropagation()
}
let redraw = 0
let itemsLimit = 50
$: validateInput(pattern, value, required)
let redraw = $state(0)
let itemsLimit = $state(50)
let oldValue = value
@@ -404,7 +452,48 @@
}
let debounced = debounce(() => compareValues(value), 50)
$: shouldDispatchChanges && debounced(value)
let inputCat = $derived(computeInputCat(type, format, itemsType?.type, enum_, contentEncoding))
$effect(() => {
oneOf && untrack(() => updateOneOfSelected(oneOf))
})
$effect(() => {
oneOf && value && untrack(() => onOneOfChange())
})
$effect(() => {
value
let args = [inputCat, defaultValue, nullable]
untrack(() => {
if (deepEqual(value, lastValue)) return
computeDefaultValue(...args)
})
})
$effect(() => {
!isListJson &&
inputCat === 'list' &&
value != lastValue &&
itemsType?.type &&
!hasIsListJsonChanged &&
untrack(() => checkArrayValueType())
})
$effect(() => {
defaultValue != undefined && untrack(() => handleDefaultValueChange())
})
$effect(() => {
;(inputCat &&
(isObjectCat(inputCat) || isRawStringEditor(inputCat)) &&
!oneOf &&
untrack(() => evalValueToRaw())) ||
value
})
$effect(() => {
let args = [pattern, value, required] as const
untrack(() => validateInput(...args))
})
$effect(() => {
shouldDispatchChanges && debounced(value)
})
</script>
<S3FilePicker
@@ -417,7 +506,7 @@
readOnlyMode={false}
/>
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<div
class={twMerge(
'flex flex-col w-full rounded-md relative',
@@ -441,17 +530,21 @@
>
<button
class="p-1 bg-green-500 text-white hover:bg-green-600"
on:click|preventDefault|stopPropagation={() => {
dispatch('acceptChange', { label, nestedParent })
}}
onclick={stopPropagation(
preventDefault(() => {
dispatch('acceptChange', { label, nestedParent })
})
)}
>
<Check size={14} />
</button>
<button
class="p-1 hover:bg-red-500 hover:text-white"
on:click|preventDefault|stopPropagation={() => {
dispatch('rejectChange', { label, nestedParent })
}}
onclick={stopPropagation(
preventDefault(() => {
dispatch('rejectChange', { label, nestedParent })
})
)}
>
<X size={14} />
</button>
@@ -502,11 +595,11 @@
<div class="relative w-full">
<input
{autofocus}
on:focus
on:blur
onfocus={bubble('focus')}
onblur={bubble('blur')}
{disabled}
type="number"
on:keydown={() => {
onkeydown={() => {
ignoreValueUndefined = true
}}
class={valid
@@ -545,6 +638,9 @@
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
{disabled}
bind:selected={value}
onremove={(e) => {
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
}}
options={itemsType?.multiselect ?? []}
selectedOptionsDraggable={true}
onopen={() => {
@@ -552,14 +648,17 @@
}}
/>
</div>
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && Array.isArray(value)}
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && (Array.isArray(value) || value == undefined)}
<div class="items-start">
<Multiselect
ulOptionsClass={'p-2 !bg-surface-secondary'}
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
{disabled}
onremove={(e) => {
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
}}
bind:selected={
() => [...value],
() => [...(value ?? [])],
(v) => {
if (!deepEqual(v, value)) {
value = v
@@ -610,7 +709,7 @@
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
onchange={(x) => fileChanged(x, (val) => (value[i] = val))}
multiple={false}
/>
{:else if itemsType?.type == 'object' && itemsType?.resourceType === undefined && itemsType?.properties === undefined}
@@ -624,7 +723,6 @@
{/await}
{:else if Array.isArray(itemsType?.enum)}
<ArgEnum
required
create={extra['disableCreate'] != true}
on:focus={() => {
dispatch('focus')
@@ -633,7 +731,7 @@
dispatch('blur')
}}
{defaultValue}
{valid}
valid={valid ?? true}
{disabled}
{autofocus}
bind:value={value[i]}
@@ -680,7 +778,7 @@
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
aria-label="Clear"
on:click={() => {
onclick={() => {
value = value.filter((_, index) => index !== i)
redraw += 1
}}
@@ -691,7 +789,7 @@
{/if}
{/each}
{#if value.length > itemsLimit}
<button on:click={() => (itemsLimit += 50)} class="text-xs py-2 text-blue-600"
<button onclick={() => (itemsLimit += 50)} class="text-xs py-2 text-blue-600"
>{itemsLimit}/{value.length}: Load 50 more...</button
>
{/if}
@@ -754,16 +852,16 @@
args={otherArgs}
{helperScript}
bind:value
entrypoint={format.substring('dynselect_'.length)}
entrypoint={format?.substring('dynselect_'.length) ?? ''}
/>
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
<span class="text-2xs text-tertiary">Loading resource types...</span>
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format.split('-').length > 1 && resourceTypes.includes(format.substring('resource-'.length))))}
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))}
<ObjectResourceInput
{defaultValue}
selectFirst={!noDefaultOnSelectFirst}
{disablePortal}
{format}
format={format ?? ''}
bind:value
bind:editor
on:clear={() => {
@@ -771,7 +869,7 @@
}}
{showSchemaExplorer}
/>
{:else if inputCat == 'resource-object' && format.split('-').length > 1 && format
{:else if inputCat == 'resource-object' && format && format.split('-').length > 1 && format
.replace('resource-', '')
.replace('_', '')
.toLowerCase() == 's3object'}
@@ -855,11 +953,12 @@
: 'label'
value = { ...toKeep, [tagKey]: detail }
}}
let:item
>
{#each oneOf as obj}
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
{/each}
{#snippet children({ item })}
{#each oneOf as obj}
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
{/each}
{/snippet}
</ToggleButtonGroup>
{#if oneOfSelected}
{@const objIdx = oneOf.findIndex((o) => o.title === oneOfSelected)}
@@ -873,13 +972,18 @@
{onlyMaskPassword}
{disablePortal}
{disabled}
schema={{
properties: obj.properties,
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}}
bind:schema={
() => ({
properties: obj.properties ?? {},
order: obj.order,
$schema: '',
required: obj.required ?? [],
type: 'object'
}),
() => {
dispatch('nestedChange')
}
}
bind:args={value}
dndType={`nested-${title}`}
hiddenArgs={['label', 'kind']}
@@ -889,9 +993,6 @@
oneOf[objIdx].order = keys
}
}}
on:change={() => {
dispatch('nestedChange')
}}
on:nestedChange
{shouldDispatchChanges}
/>
@@ -909,7 +1010,12 @@
required: obj.required ?? [],
type: 'object'
}}
bind:args={value}
bind:args={
() => value,
(v) => {
value = { ...v, [tagKey]: oneOfSelected }
}
}
{shouldDispatchChanges}
on:change={() => {
dispatch('nestedChange')
@@ -971,22 +1077,24 @@
{onlyMaskPassword}
{disablePortal}
{disabled}
schema={{
properties,
$schema: '',
required: nestedRequired ?? [],
type: 'object',
order
}}
bind:schema={
() => ({
properties,
$schema: '',
required: nestedRequired ?? [],
type: 'object',
order
}),
(newSchema) => {
dispatch('nestedChange')
}
}
bind:args={value}
dndType={`nested-${title}`}
on:reorder={(e) => {
const keys = e.detail
order = keys
}}
on:change={() => {
dispatch('nestedChange')
}}
diff={diffStatus && typeof diffStatus.diff === 'object' ? diffStatus.diff : {}}
on:acceptChange={(e) => {
dispatch('acceptChange', e.detail)
@@ -1065,10 +1173,9 @@
{:else if inputCat == 'enum'}
<div class="flex flex-row w-full gap-1">
<ArgEnum
{required}
create={extra['disableCreate'] != true}
{defaultValue}
{valid}
valid={valid ?? true}
{disabled}
bind:value
{enum_}
@@ -1119,7 +1226,7 @@
<input
{autofocus}
type="file"
on:change={(x) => fileChanged(x, (val) => (value = val))}
onchange={(x) => fileChanged(x, (val) => (value = val))}
multiple={false}
/>
{#if value?.length}
@@ -1144,8 +1251,8 @@
{:else if inputCat == 'email'}
<input
{autofocus}
on:focus
on:blur
onfocus={bubble('focus')}
onblur={bubble('blur')}
{disabled}
type="email"
class={valid
@@ -1177,14 +1284,14 @@
{autofocus}
rows={extra?.['minRows'] ? extra['minRows']?.toString() : '1'}
bind:this={el}
on:focus={(e) => {
onfocus={(e) => {
dispatch('focus')
}}
on:blur={(e) => {
onblur={(e) => {
dispatch('blur')
}}
use:autosize
on:keydown={onKeyDown}
onkeydown={onKeyDown}
{disabled}
class={twMerge(
'w-full',
@@ -1197,10 +1304,10 @@
></textarea>
{/key}
{#if !disabled && itemPicker && extra?.['disableVariablePicker'] != true}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<button
class="absolute right-1 top-1 py-1 min-w-min !px-2 items-center text-gray-800 bg-surface-secondary border rounded center-center hover:bg-gray-300 transition-all cursor-pointer"
on:click={() => {
onclick={() => {
pickForField = label
itemPicker?.openDrawer?.()
}}
@@ -1216,7 +1323,7 @@
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
Linked to variable <button
class="text-blue-500 underline"
on:click={() => variableEditor?.editVariable?.(value.slice(5))}
onclick={() => variableEditor?.editVariable?.(value.slice(5))}
>{value.slice(5)}</button
>
{/if}
@@ -1224,7 +1331,7 @@
{/if}
</div>
{/if}
<slot name="actions" />
{@render actions?.()}
</div>
{#if !compact || (error && error != '')}
@@ -6,11 +6,11 @@
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
export let value: any
interface Props {
value: any
}
$: enabled = value != undefined
$: changeDomain(value?.['domain'], value?.['custom'])
let { value = $bindable() }: Props = $props()
function changeDomain(domain, custom) {
if (value) {
@@ -31,10 +31,14 @@
}
}
}
let enabled = $derived(value != undefined)
$effect(() => {
changeDomain(value?.['domain'], value?.['custom'])
})
</script>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-sm font-medium text-primary flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name="auth0" after={true} /></div><Toggle
checked={enabled}
@@ -57,10 +61,11 @@
on:selected={({ detail }) => {
value['custom'] = detail === 'custom'
}}
let:item
>
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{#snippet children({ item })}
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
<div class="grow">
@@ -11,11 +11,15 @@
import Label from './Label.svelte'
import MultiSelect from 'svelte-multiselect'
export let config: AutoscalingConfig | undefined
export let worker_tags: string[] | undefined
interface Props {
config: AutoscalingConfig | undefined
worker_tags: string[] | undefined
}
let { config = $bindable(), worker_tags }: Props = $props()
const dispatch = createEventDispatcher()
let test_input: number = 3
let test_input: number = $state(3)
</script>
<div class="flex flex-row gap-16 pt-2">
@@ -49,11 +53,11 @@
}
}}
/>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Min # of Workers
{#if config !== undefined}
<input on:input={() => dispatch('dirty')} type="number" bind:value={config.min_workers} />
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.min_workers} />
{#if config.min_workers !== undefined && config.min_workers != undefined && config.min_workers > config.max_workers}
<div class="text-red-600 text-xs whitespace-nowrap"
>Minimum cannot be {'>'} to Maximum</div
@@ -63,11 +67,11 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Max # of Workers
{#if config !== undefined}
<input on:input={() => dispatch('dirty')} type="number" bind:value={config.max_workers} />
<input oninput={() => dispatch('dirty')} type="number" bind:value={config.max_workers} />
{:else}
<input type="number" disabled />
{/if}
@@ -75,12 +79,12 @@
<div class="p-2">
<Section label="Advanced" small collapsable={true}>
<div class="flex flex-col gap-2 text-2xs">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after an incremental scale-in/out
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
@@ -91,12 +95,12 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Cooldown seconds after a full scale out
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="30"
@@ -107,12 +111,12 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger an incremental scale-out
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
bind:value={config.inc_scale_num_jobs_waiting}
placeholder="1"
@@ -122,14 +126,14 @@
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num jobs waiting to trigger a full scale out <Tooltip
>Default: max_workers, full scale out = scale out to max workers</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
placeholder="max workers"
bind:value={config.full_scale_jobs_waiting}
@@ -138,14 +142,14 @@
<input type="number" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate % threshold to go below to trigger a scale-in (decrease) <Tooltip
>Default: 25%, need to go below average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
@@ -157,14 +161,14 @@
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Occupancy rate threshold to exceed to trigger an incremental scale-out (increase) <Tooltip
>Default: 75%, need to exceed average of all of 15s, 5m and 30m occupancy rates</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="0"
@@ -176,14 +180,14 @@
<input type="number" step="0.01" disabled />
{/if}
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
Num workers to scale-in/out by when incremental <Tooltip
>Default: (max_workers - min_workers) / 5</Tooltip
>
{#if config !== undefined}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="number"
step="1"
min="1"
@@ -196,12 +200,12 @@
</label>
<Label label="Custom tags to autoscale on">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
By default, autoscaling will apply to the tags the worker group is assigned to but
you can override this here.
</Tooltip>
</svelte:fragment>
{/snippet}
{#if config}
{#if config.custom_tags}
<MultiSelect
@@ -259,30 +263,31 @@
on:selected={(e) => dispatch('dirty')}
bind:selected={config.integration.type}
class="mb-4 mt-2"
let:item
>
<ToggleButton
value="dryrun"
label="Dry run"
tooltip="See autoscaling events but not actual scaling actions will be performed"
{item}
/>
<ToggleButton
value="script"
label="Custom script"
tooltip="Run a custom script to scale your worker group"
{item}
/>
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
{#snippet children({ item })}
<ToggleButton
value="dryrun"
label="Dry run"
tooltip="See autoscaling events but not actual scaling actions will be performed"
{item}
/>
<ToggleButton
value="script"
label="Custom script"
tooltip="Run a custom script to scale your worker group"
{item}
/>
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
{/snippet}
</ToggleButtonGroup>
{#if config.integration.type === 'script'}
<label>
Script path on the 'admins' workspace
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.path}
/>
@@ -292,7 +297,7 @@
Custom tag for executing script (optional)
{#if config.integration.tag}
<input
on:input={() => dispatch('dirty')}
oninput={() => dispatch('dirty')}
type="text"
bind:value={config.integration.tag}
/>
@@ -337,12 +342,14 @@
</div>
{/if}
{:else}
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2" let:item>
<ToggleButton value="dryrun" label="Dry run" {item} />
<ToggleButton value="script" label="Custom script" {item} />
<ToggleButton value="ecs" label="ECS (soon)" {item} />
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
{#snippet children({ item })}
<ToggleButton value="dryrun" label="Dry run" {item} />
<ToggleButton value="script" label="Custom script" {item} />
<ToggleButton value="ecs" label="ECS (soon)" {item} />
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
{/snippet}
</ToggleButtonGroup>
<label>
@@ -18,10 +18,14 @@
import YAML from 'yaml'
import { twMerge } from 'tailwind-merge'
import ContentSearchInnerItem from './ContentSearchInnerItem.svelte'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
const dispatch = createEventDispatcher()
export let search: string = ''
interface Props {
search?: string
}
let { search = $bindable('') }: Props = $props()
export async function open(nsearch?: string) {
await Promise.all([loadScripts(), loadResources(), loadApps(), loadFlows()])
@@ -46,19 +50,19 @@
flows = await FlowService.listSearchFlow({ workspace: $workspaceStore ?? '' })
}
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = 'all'
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = $state('all')
let scripts: undefined | { path: string; content: string }[] = undefined
let filteredScriptItems: { path: string; content: string; marked: any }[] = []
let scripts: undefined | { path: string; content: string }[] = $state(undefined)
let filteredScriptItems: { path: string; content: string; marked: any }[] = $state([])
let resources: undefined | { path: string; value: any }[] = undefined
let filteredResourceItems: { path: string; value: any; marked: any }[] = []
let resources: undefined | { path: string; value: any }[] = $state(undefined)
let filteredResourceItems: { path: string; value: any; marked: any }[] = $state([])
let flows: undefined | { path: string; value: any }[] = undefined
let filteredFlowItems: { path: string; value: any; marked: any }[] = []
let flows: undefined | { path: string; value: any }[] = $state(undefined)
let filteredFlowItems: { path: string; value: any; marked: any }[] = $state([])
let apps: undefined | { path: string; value: any }[] = undefined
let filteredAppItems: { path: string; value: any; marked: any }[] = []
let apps: undefined | { path: string; value: any }[] = $state(undefined)
let filteredAppItems: { path: string; value: any; marked: any }[] = $state([])
function getCounts(n: number) {
return ` (${n})`
@@ -73,23 +77,34 @@
.replace(/'/g, '&#39;')
}
$: counts =
let showNbScripts = $state(10)
let showNbApps = $state(10)
let showNbResources = $state(10)
let showNbFlows = $state(10)
function resetShows() {
showNbScripts = 10
showNbApps = 10
showNbResources = 10
showNbFlows = 10
}
let counts = $derived(
search == '' ||
!scripts ||
!resources ||
!flows ||
!apps ||
!filteredAppItems ||
!filteredFlowItems ||
!filteredResourceItems ||
!filteredScriptItems
!scripts ||
!resources ||
!flows ||
!apps ||
!filteredAppItems ||
!filteredFlowItems ||
!filteredResourceItems ||
!filteredScriptItems
? {
all: '',
apps: '',
flows: '',
resources: '',
scripts: ''
}
}
: {
all: getCounts(
filteredAppItems.length +
@@ -101,21 +116,11 @@
resources: getCounts(filteredResourceItems.length),
flows: getCounts(filteredFlowItems.length),
scripts: getCounts(filteredScriptItems.length)
}
let showNbScripts = 10
let showNbApps = 10
let showNbResources = 10
let showNbFlows = 10
$: search && resetShows()
function resetShows() {
showNbScripts = 10
showNbApps = 10
showNbResources = 10
showNbFlows = 10
}
}
)
$effect(() => {
search && untrack(() => resetShows())
})
</script>
<SearchItems
@@ -157,42 +162,44 @@
<div class="flex flex-col gap-2">
<div class="flex gap-2 flex-wrap sticky top-0 left-0 right-0 bg-surface">
<div class="p-2">
<ToggleButtonGroup bind:selected={searchKind} class="h-10 " let:item>
<ToggleButton small light value="all" label={'All' + counts.all} {item} />
<ToggleButton
small
light
value="scripts"
icon={Code2}
label={'Scripts' + counts.scripts}
{item}
/>
<ToggleButton
small
light
value="resources"
icon={Boxes}
label={'Resources' + counts.resources}
{item}
/>
<ToggleButton
small
light
value="flows"
label={'Flows' + counts.flows}
icon={FlowIcon}
selectedColor="#14b8a6"
{item}
/>
<ToggleButton
small
light
value="apps"
label={'Apps' + counts.apps}
icon={LayoutDashboard}
selectedColor="#fb923c"
{item}
/>
<ToggleButtonGroup bind:selected={searchKind} class="h-10 ">
{#snippet children({ item })}
<ToggleButton small light value="all" label={'All' + counts.all} {item} />
<ToggleButton
small
light
value="scripts"
icon={Code2}
label={'Scripts' + counts.scripts}
{item}
/>
<ToggleButton
small
light
value="resources"
icon={Boxes}
label={'Resources' + counts.resources}
{item}
/>
<ToggleButton
small
light
value="flows"
label={'Flows' + counts.flows}
icon={FlowIcon}
selectedColor="#14b8a6"
{item}
/>
<ToggleButton
small
light
value="apps"
label={'Apps' + counts.apps}
icon={LayoutDashboard}
selectedColor="#fb923c"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
</div>
@@ -245,7 +252,7 @@
href={`/scripts/get/${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/scripts/get/${item.path}`}
color="light"
@@ -267,7 +274,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border rounded-md p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code>
</pre>
@@ -295,7 +302,7 @@
href={`/resources#${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/resources#${item.path}`}
color="light"
@@ -305,7 +312,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border rounded-md p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
>
@@ -333,7 +340,7 @@
href={`/flows/get/${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/flows/get/${item.path}`}
color="light"
@@ -354,7 +361,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
@@ -383,7 +390,7 @@
href={`/apps/get/${item.path}`}
on:close
>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
href={`/apps/get/${item.path}`}
color="light"
@@ -404,7 +411,7 @@
>
Edit
</Button>
</svelte:fragment>
{/snippet}
<pre class="text-xs border p-2 overflow-auto max-h-40 w-full"
><code>{@html item.marked}</code></pre
@@ -1,8 +1,14 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let title: string
export let href: string
interface Props {
title: string
href: string
actions?: import('svelte').Snippet
children?: import('svelte').Snippet
}
let { title, href, actions, children }: Props = $props()
const dispatch = createEventDispatcher()
</script>
@@ -12,7 +18,7 @@
<a
class="text-sm text-blue-500 truncate hover:underline"
{href}
on:click={() => {
onclick={() => {
dispatch('close')
}}
>
@@ -20,8 +26,8 @@
</a>
<div class="flex flex-row gap-2 items-center">
<slot name="actions" />
{@render actions?.()}
</div>
</div>
<slot />
{@render children?.()}
</div>
@@ -69,7 +69,7 @@
<textarea rows="4" use:autosize bind:value placeholder="Variable value"></textarea>
</Section>
</div>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
on:click={() => updateVariable()}
disabled={value === '' || name === ''}
@@ -79,6 +79,6 @@
>
{edit ? 'Update' : 'Save'}
</Button>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
@@ -249,7 +249,7 @@
</Pane>
</Splitpanes>
{/if}
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
loading={refreshing}
on:click={() => refresh()}
@@ -266,7 +266,7 @@
size="xs"
color="light"
/>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
{/if}
@@ -24,10 +24,11 @@
class="mb-4"
selected={dbSchema.publicOnly ? 'dbo' : 'all'}
on:selected={handleSelected}
let:item
>
<ToggleButton value="dbo" label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} {item} />
<ToggleButton value="all" label="All" {item} />
{#snippet children({ item })}
<ToggleButton value="dbo" label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} {item} />
<ToggleButton value="all" label="All" {item} />
{/snippet}
</ToggleButtonGroup>
{/if}
{#if dbSchema.lang === 'graphql'}
+3 -2
View File
@@ -13,6 +13,7 @@
import { deepEqual } from 'fast-equals'
import 'ag-grid-community/styles/ag-grid.css'
import '$lib/components/apps/components/display/table/theme/windmill-theme.css'
import { untrack } from 'svelte'
type Props = {
dbTableOps: IDbTableOps
@@ -45,7 +46,7 @@
let refreshCount = $state(0)
const refresh = () => (refreshCount += 1)
$effect(() => eGui && mountGrid())
$effect(() => eGui && untrack(() => mountGrid()))
function mountGrid() {
if (eGui && !api) {
createGrid(eGui, {
@@ -95,7 +96,7 @@
const key = { quicksearch, colDefs: dbTableOps.colDefs, refreshCount }
if (deepEqual(key, prevUpdateKey)) return
prevUpdateKey = key
updateGrid()
untrack(() => updateGrid())
})
function updateGrid() {
dbTableOps.getCount({ quicksearch }).then((result) => (rowCount = result))
@@ -1,24 +1,39 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import { createEventDispatcher, untrack } from 'svelte'
import { Button } from './common'
import { Clock, X } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
// import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
// import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
export let value: string | undefined = undefined
interface Props {
// import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
value?: string | undefined
clearable?: boolean
autofocus?: boolean | null
useDropdown?: boolean
minDate?: string | undefined
maxDate?: string | undefined
disabled?: boolean | undefined
inputClass?: string | undefined
}
export let clearable: boolean = false
export let autofocus: boolean | null = false
export let useDropdown: boolean = false
export let minDate: string | undefined = undefined
export let maxDate: string | undefined = undefined
export let disabled: boolean | undefined = undefined
export let inputClass: string | undefined = undefined
let {
value = $bindable(undefined),
clearable = false,
autofocus = false,
useDropdown = false,
minDate = undefined,
maxDate = undefined,
disabled = undefined,
inputClass = undefined
}: Props = $props()
let date: string | undefined = undefined
let time: string | undefined = undefined
let date: string | undefined = $state(undefined)
let time: string | undefined = $state(undefined)
// let format: 'local' | 'utc' = 'local'
@@ -38,10 +53,15 @@
: '12:00'
}
$: parseValue(value)
$effect(() => {
value
untrack(() => {
parseValue(value)
})
})
let initialDate = date
let initialTime = time
let initialDate = untrack(() => date)
let initialTime = untrack(() => time)
function parseDateAndTime(date: string | undefined, time: string | undefined) {
if (date && time && (initialDate != date || initialTime != time)) {
@@ -54,7 +74,12 @@
}
}
$: parseDateAndTime(date, time)
$effect(() => {
;[date, time]
untrack(() => {
parseDateAndTime(date, time)
})
})
function isValidDate(d: Date | undefined): boolean {
return d instanceof Date && !isNaN(d as any)
@@ -73,8 +98,13 @@
let randomId = 'datetarget-' + Math.random().toString(36).substring(7)
</script>
<div class="flex flex-row gap-1 items-center w-full" id={randomId} on:pointerdown on:focus>
<!-- svelte-ignore a11y-autofocus -->
<div
class="flex flex-row gap-1 items-center w-full"
id={randomId}
onpointerdown={bubble('pointerdown')}
onfocus={bubble('focus')}
>
<!-- svelte-ignore a11y_autofocus -->
<input
type="date"
bind:value={date}
@@ -36,11 +36,11 @@
/>
{/if}
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
disabled={workspaceToDeployTo == undefined}
on:click={() => deployWorkspace?.deployAll()}>Deploy All</Button
>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+74 -65
View File
@@ -11,7 +11,6 @@
OpenAPI,
type Preview,
type OpenFlow,
type FlowModule,
WorkspaceService,
type InputTransform,
type TriggersCount
@@ -20,7 +19,7 @@
import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores'
import { emptySchema, sendUserToast } from '$lib/utils'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { onDestroy, onMount, setContext } from 'svelte'
import { onDestroy, onMount, setContext, untrack } from 'svelte'
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
import { page } from '$app/stores'
import { getUserExt } from '$lib/user'
@@ -49,16 +48,6 @@
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
import type { PickableProperties } from './flows/previousResults'
import { Triggers } from './triggers/triggers.svelte'
$: token = $page.url.searchParams.get('wm_token') ?? undefined
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
$: themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
$: themeDark = themeDarkRaw == '2' || themeDarkRaw == '4'
$: if (token) {
OpenAPI.WITH_CREDENTIALS = true
OpenAPI.TOKEN = token
loadUser()
}
let flowCopilotContext: FlowCopilotContext = {
shouldUpdatePropertyType: writable<{
@@ -87,14 +76,6 @@
}
}
}
$: if (workspace) {
$workspaceStore = workspace
setupCopilotInfo()
}
$: if (workspace && token) {
loadUser()
}
async function loadUser() {
try {
@@ -105,31 +86,26 @@
}
}
let darkModeToggle: DarkModeToggle
let darkMode: boolean | undefined = undefined
let modeInitialized = false
let darkModeToggle: DarkModeToggle | undefined = $state()
let darkMode: boolean | undefined = $state(undefined)
let modeInitialized = $state(false)
function initializeMode() {
modeInitialized = true
darkModeToggle.toggle()
darkModeToggle?.toggle()
}
$: darkModeToggle &&
themeDark != darkMode &&
darkMode != undefined &&
!modeInitialized &&
initializeMode()
let testJobLoader: TestJobLoader
let testJobLoader: TestJobLoader | undefined = $state()
let socket: WebSocket | undefined = undefined
// Test args input
let args: Record<string, any> = {}
let isValid: boolean = true
let args: Record<string, any> = $state({})
let isValid: boolean = $state(true)
// Test
let testIsLoading = false
let testJob: Job | undefined
let pastPreviews: CompletedJob[] = []
let validCode = true
let testIsLoading = $state(false)
let testJob: Job | undefined = $state()
let pastPreviews: CompletedJob[] = $state([])
let validCode = $state(true)
type LastEditScript = {
content: string
@@ -140,9 +116,9 @@
tag?: string
}
let currentScript: LastEditScript | undefined = undefined
let currentScript: LastEditScript | undefined = $state(undefined)
let schema = emptySchema()
let schema = $state(emptySchema())
const href = window.location.href
const indexQ = href.indexOf('?')
const searchParams = indexQ > -1 ? new URLSearchParams(href.substring(indexQ)) : undefined
@@ -151,12 +127,12 @@
connectWs()
}
let useLock = false
let useLock = $state(false)
let lockChanges = false
let timeout: NodeJS.Timeout | undefined = undefined
let loadingCodebaseButton = false
let loadingCodebaseButton = $state(false)
let lastCommandId = ''
const el = (event) => {
@@ -174,7 +150,7 @@
}
} else if (event.data.type == 'testPreviewBundle') {
if (event.data.id == lastCommandId && currentScript) {
testJobLoader.runPreview(
testJobLoader?.runPreview(
currentScript.path,
event.data.file,
currentScript.language,
@@ -333,7 +309,7 @@
}
}
let typescriptBundlePreviewMode = false
let typescriptBundlePreviewMode = $state(false)
function runTest() {
if (mode == 'script') {
if (!currentScript) {
@@ -391,7 +367,7 @@
}
}
let relativePaths: any[] = []
let relativePaths: any[] = $state([])
let lastPath: string | undefined = undefined
async function replaceScript(lastEdit: LastEditScript) {
mode = 'script'
@@ -416,14 +392,16 @@
}
}
let mode: 'script' | 'flow' = 'script'
let mode: 'script' | 'flow' = $state('script')
const flowStore = writable({
summary: '',
value: { modules: [] },
extra_perms: {},
schema: emptySchema()
} as OpenFlow)
const flowStore = $state({
val: {
summary: '',
value: { modules: [] },
extra_perms: {},
schema: emptySchema()
} as OpenFlow
})
type LastEditFlow = {
flow: OpenFlow
@@ -436,14 +414,14 @@
// sendUserToast(JSON.stringify(lastEdit.flow), true)
// return
try {
if (!deepEqual(lastEdit.flow, $flowStore)) {
if (!deepEqual(lastEdit.flow, flowStore)) {
if (!lastEdit.flow.summary) {
lastEdit.flow.summary = 'New flow'
}
if (!lastEdit.flow.value?.modules) {
lastEdit.flow.value = { modules: [] }
}
$flowStore = lastEdit.flow
flowStore.val = lastEdit.flow
inferModuleArgs($selectedIdStore)
}
} catch (e) {
@@ -453,10 +431,10 @@
const flowStateStore = writable({} as FlowState)
const previewArgsStore = writable<Record<string, any>>({})
const previewArgsStore = $state({ val: {} })
const scriptEditorDrawer = writable(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
const history = initHistory($flowStore)
const moving = writable<{ id: string } | undefined>(undefined)
const history = initHistory(flowStore.val)
const testStepStore = writable<Record<string, any>>({})
const selectedIdStore = writable('settings-metadata')
@@ -496,7 +474,6 @@
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
pickablePropertiesFiltered: writable<PickableProperties | undefined>(undefined)
})
$: updateFlow($flowStore)
let lastSent: OpenFlow | undefined = undefined
function updateFlow(flow: OpenFlow) {
@@ -509,17 +486,15 @@
}
}
$: $selectedIdStore && inferModuleArgs($selectedIdStore)
let flowPreviewButtons: FlowPreviewButtons
let reload = 0
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
let reload = $state(0)
async function inferModuleArgs(selectedIdStore: string) {
if (selectedIdStore == '') {
return
}
//@ts-ignore
dfs($flowStore.value.modules, async (mod) => {
dfs(flowStore.value.modules, async (mod) => {
if (mod.id == selectedIdStore) {
if (
mod.value.type == 'rawscript' ||
@@ -544,9 +519,44 @@
}
})
}
let token = $derived($page.url.searchParams.get('wm_token') ?? undefined)
let workspace = $derived($page.url.searchParams.get('workspace') ?? undefined)
let themeDarkRaw = $derived($page.url.searchParams.get('activeColorTheme'))
let themeDark = $derived(themeDarkRaw == '2' || themeDarkRaw == '4')
$effect(() => {
if (token) {
OpenAPI.WITH_CREDENTIALS = true
OpenAPI.TOKEN = token
untrack(() => loadUser())
}
})
$effect(() => {
if (workspace) {
$workspaceStore = workspace
untrack(() => setupCopilotInfo())
}
})
$effect(() => {
if (workspace && token) {
untrack(() => loadUser())
}
})
$effect(() => {
darkModeToggle &&
themeDark != darkMode &&
darkMode != undefined &&
!modeInitialized &&
untrack(() => initializeMode())
})
$effect(() => {
flowStore.val && untrack(() => updateFlow(flowStore.val))
})
$effect(() => {
$selectedIdStore && untrack(() => inferModuleArgs($selectedIdStore))
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<TestJobLoader
on:done={loadPastTests}
@@ -686,10 +696,9 @@
</div>
<Splitpanes horizontal class="h-full max-h-screen grow">
<Pane size={67}>
{#if $flowStore?.value?.modules}
{#if flowStore.val?.value?.modules}
<div id="flow-editor"></div>
<FlowModuleSchemaMap
bind:modules={$flowStore.value.modules}
disableAi
disableTutorials
smallErrorHandler={true}
@@ -709,7 +718,7 @@
$testStepStore['preprocessor'] = ev.detail.args ?? {}
$selectedIdStore = 'preprocessor'
} else {
$previewArgsStore = ev.detail.args ?? {}
previewArgsStore.val = ev.detail.args ?? {}
flowPreviewButtons?.openPreview()
}
}}
@@ -55,7 +55,6 @@
diffViewer.closeDrawer()
}
function prepareDiff(data: Value) {
const metadata = structuredClone(cleanValueProperties(replaceFalseWithUndefined(data)))
const content = metadata['content']
@@ -126,8 +125,8 @@
data_[dataType]?.content !== data_.current.content
? 'content'
: data_[dataType]?.metadata !== data_.current.metadata
? 'metadata'
: undefined
? 'metadata'
: undefined
}
$: updateContentType(data, diffType)
@@ -256,7 +255,7 @@
<Loader2 class="animate-spin" />
{/if}
</div>
<svelte:fragment slot="actions">
{#snippet actions()}
{#if data?.button}
<Button
color="light"
@@ -268,6 +267,6 @@
}}>{data.button.text}</Button
>
{/if}
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+102 -70
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import DisplayResult from './DisplayResult.svelte'
import { Highlight } from 'svelte-highlight'
import { json } from 'svelte-highlight/languages'
import { copyToClipboard, roughSizeOfObject } from '$lib/utils'
@@ -38,22 +40,6 @@
import { toJsonStr } from '$lib/utils'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let result: any
export let requireHtmlApproval = false
export let filename: string | undefined = undefined
export let disableExpand = false
export let jobId: string | undefined = undefined
export let workspaceId: string | undefined = undefined
export let hideAsJson: boolean = false
export let noControls: boolean = false
export let drawerOpen = false
export let nodeId: string | undefined = undefined
export let language: string | undefined = undefined
export let appPath: string | undefined = undefined
export let customUi: DisplayResultUi | undefined = undefined
export let isTest: boolean = true
export let externalToolbarAvailable: boolean = false
const IMG_MAX_SIZE = 10000000
const TABLE_MAX_SIZE = 5000000
const DISPLAY_MAX_SIZE = 100000
@@ -82,14 +68,53 @@
| 'map'
| 'nondisplayable'
| 'pdf'
| undefined
| undefined = $state()
let hasBigInt = false
$: resultKind = inferResultKind(result)
let hasBigInt = $state(false)
export let forceJson = false
let enableHtml = false
let s3FileDisplayRawMode = false
interface Props {
result: any
requireHtmlApproval?: boolean
filename?: string | undefined
disableExpand?: boolean
jobId?: string | undefined
workspaceId?: string | undefined
hideAsJson?: boolean
noControls?: boolean
drawerOpen?: boolean
nodeId?: string | undefined
language?: string | undefined
appPath?: string | undefined
customUi?: DisplayResultUi | undefined
isTest?: boolean
externalToolbarAvailable?: boolean
forceJson?: boolean
copilot_fix?: import('svelte').Snippet
children?: import('svelte').Snippet
}
let {
result,
requireHtmlApproval = false,
filename = undefined,
disableExpand = false,
jobId = undefined,
workspaceId = undefined,
hideAsJson = false,
noControls = false,
drawerOpen = $bindable(false),
nodeId = undefined,
language = undefined,
appPath = undefined,
customUi = undefined,
isTest = true,
externalToolbarAvailable = false,
forceJson = $bindable(false),
copilot_fix,
children
}: Props = $props()
let enableHtml = $state(false)
let s3FileDisplayRawMode = $state(false)
function isTableRow(result: any): boolean {
return Array.isArray(result) && result.every((x) => Array.isArray(x))
@@ -125,14 +150,14 @@
)
}
let largeObject: boolean | undefined = undefined
let largeObject: boolean | undefined = $state(undefined)
function checkIfS3(result: any, keys: string[]) {
return keys.includes('s3') && typeof result.s3 === 'string'
}
let is_render_all = false
let download_as_csv = false
let is_render_all = $state(false)
let download_as_csv = $state(false)
function inferResultKind(result: any) {
try {
if (result === 'WINDMILL_TOO_BIG') {
@@ -267,8 +292,8 @@
return 'json'
}
let jsonViewer: Drawer
let s3FileViewer: S3FilePicker
let jsonViewer: Drawer | undefined = $state()
let s3FileViewer: S3FilePicker | undefined = $state()
function checkIfHasBigInt(result: any) {
if (typeof result === 'number' && Number.isInteger(result) && !Number.isSafeInteger(result)) {
@@ -386,20 +411,20 @@
}
export function openDrawer() {
jsonViewer.openDrawer()
jsonViewer?.openDrawer()
}
let globalForceJson: boolean = false
let globalForceJson: boolean = $state(false)
let seeS3PreviewFileFromList = ''
let seeS3PreviewFileFromList = $state('')
const disableTooltips = hasContext('disableTooltips')
? getContext('disableTooltips') === true
: false
let resultHeaderHeight = 0
let resultHeaderHeight = $state(0)
let toolbarLocation: 'self' | 'external' | undefined = undefined
let toolbarLocation: 'self' | 'external' | undefined = $state(undefined)
function chooseToolbarLocation(shouldShowToolbar: boolean, resultHeaderHeight: number) {
if (!shouldShowToolbar) {
toolbarLocation = undefined
@@ -415,20 +440,25 @@
return toolbarLocation
}
$: chooseToolbarLocation(
!is_render_all &&
resultKind != 'nondisplayable' &&
result != undefined &&
length != undefined &&
largeObject != undefined &&
!disableExpand &&
!noControls,
resultHeaderHeight
)
onDestroy(() => {
dispatch('toolbar-location-changed', undefined)
})
$effect(() => {
;[result]
resultKind = inferResultKind(result)
})
$effect(() => {
chooseToolbarLocation(
!is_render_all &&
resultKind != 'nondisplayable' &&
result != undefined &&
length != undefined &&
largeObject != undefined &&
!disableExpand &&
!noControls,
resultHeaderHeight
)
})
</script>
<HighlightTheme />
@@ -442,17 +472,18 @@
on:selected={(ev) => {
globalForceJson = ev.detail === 'json'
}}
let:item
>
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} {item} />
{#snippet children({ item })}
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} {item} />
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
{/snippet}
</ToggleButtonGroup>
</div>
{/if}
<div class="flex flex-col w-full gap-10">
{#each result['render_all'] as res}
<svelte:self
<DisplayResult
{noControls}
result={res}
{requireHtmlApproval}
@@ -484,30 +515,31 @@
{#if !hideAsJson && !['json', 's3object'].includes(resultKind ?? '') && typeof result === 'object'}<ToggleButtonGroup
class="h-6"
selected={forceJson ? 'json' : resultKind?.startsWith('table-') ? 'table' : 'pretty'}
let:item
on:selected={(ev) => {
forceJson = ev.detail === 'json'
}}
>
{#if ['table-col', 'table-row', 'table-row-object'].includes(resultKind ?? '')}
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} {item} />
{:else}
<ToggleButton
class="px-1.5"
value="pretty"
label="Pretty"
icon={Highlighter}
{item}
/>
{/if}
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
{#snippet children({ item })}
{#if ['table-col', 'table-row', 'table-row-object'].includes(resultKind ?? '')}
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} {item} />
{:else}
<ToggleButton
class="px-1.5"
value="pretty"
label="Pretty"
icon={Highlighter}
{item}
/>
{/if}
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
{/snippet}
</ToggleButtonGroup>
{/if}
</div>
<div class="text-secondary text-xs flex gap-2.5 z-10 items-center">
{#if customUi?.disableAiFix !== true}
<slot name="copilot-fix" />
{@render copilot_fix?.()}
{/if}
{#if toolbarLocation === 'self'}
<!-- TODO : When svelte 5 is released, use a snippet to pass the toolbar to a parent -->
@@ -647,7 +679,7 @@
>{JSON.stringify(result.error.extra, null, 4)}</pre
>
{/if}
<slot />
{@render children?.()}
</div>
{#if !isTest && language === 'bun'}
<div class="pt-20"></div>
@@ -723,7 +755,7 @@
/>
<button
class="text-secondary underline text-2xs whitespace-nowrap"
on:click={() => {
onclick={() => {
s3FileViewer?.open?.(result)
}}
><span class="flex items-center gap-1"
@@ -738,7 +770,7 @@
<FileDownload {workspaceId} s3object={result} {appPath} />
<button
class="text-secondary underline text-2xs whitespace-nowrap"
on:click={() => {
onclick={() => {
s3FileViewer?.open?.(result)
}}
><span class="flex items-center gap-1"
@@ -813,7 +845,7 @@
/>
<button
class="text-secondary text-2xs whitespace-nowrap"
on:click={() => {
onclick={() => {
s3FileViewer?.open?.(s3object)
}}
><span class="flex items-center gap-1"
@@ -835,7 +867,7 @@
/>{:else}
<button
class="text-secondary whitespace-nowrap flex gap-2 items-center"
on:click={() => {
onclick={() => {
seeS3PreviewFileFromList = s3object?.s3
}}
>open table preview <ArrowDownFromLine />
@@ -855,7 +887,7 @@
{:else}
<button
class="text-secondary whitespace-nowrap flex gap-2 items-center"
on:click={() => {
onclick={() => {
seeS3PreviewFileFromList = s3object?.s3
}}
>open image preview <ArrowDownFromLine />
@@ -956,7 +988,7 @@
{#if !disableExpand && !noControls}
<Drawer bind:this={jsonViewer} bind:open={drawerOpen} size="900px">
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
{#if customUi?.disableDownload !== true}
<Button
download="{filename ?? 'result'}.json"
@@ -982,8 +1014,8 @@
>
Copy to clipboard
</Button>
</svelte:fragment>
<svelte:self
{/snippet}
<DisplayResult
{noControls}
{result}
{requireHtmlApproval}
@@ -1,4 +1,7 @@
<script lang="ts">
import { createBubbler, preventDefault, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import type { Schema } from '$lib/common'
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -11,7 +14,7 @@
import FlowPropertyEditor from './schema/FlowPropertyEditor.svelte'
import PropertyEditor from './schema/PropertyEditor.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import { createEventDispatcher, tick } from 'svelte'
import { createEventDispatcher, tick, untrack } from 'svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import Label from './Label.svelte'
@@ -22,72 +25,112 @@
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
import { deepEqual } from 'fast-equals'
import { tweened } from 'svelte/motion'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let schema: Schema | any
export let hiddenArgs: string[] = []
export let args: Record<string, any> = {}
export let shouldHideNoInputs: boolean = false
export let noVariablePicker = false
export let flexWrap = false
export let uiOnly: boolean = false
export let isFlowInput: boolean = false
export let noPreview: boolean = false
export let jsonEnabled: boolean = true
export let isAppInput: boolean = false
export let displayWebhookWarning: boolean = false
export let onlyMaskPassword: boolean = false
export let dndType: string | undefined = undefined
export let editTab:
| 'inputEditor'
| 'history'
| 'savedInputs'
| 'json'
| 'captures'
| 'firstStepInputs'
| undefined
export let previewSchema: Record<string, any> | undefined = undefined
export let editPanelInitialSize: number | undefined = undefined
export let editPanelSize = 0
export let diff: Record<string, SchemaDiff> = {}
export let disableDnd: boolean = false
export let shouldDispatchChanges: boolean = false
export let isValid: boolean = true
export let customUi: EditableSchemaFormUi | undefined = undefined
// export let openEditTab: () => void = () => {}
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
let clazz: string = ''
export { clazz as class }
$: if (args == undefined || typeof args !== 'object') {
args = {}
interface Props {
schema: Schema | any
hiddenArgs?: string[]
args?: Record<string, any>
shouldHideNoInputs?: boolean
noVariablePicker?: boolean
flexWrap?: boolean
uiOnly?: boolean
isFlowInput?: boolean
noPreview?: boolean
jsonEnabled?: boolean
isAppInput?: boolean
displayWebhookWarning?: boolean
onlyMaskPassword?: boolean
dndType?: string | undefined
editTab:
| 'inputEditor'
| 'history'
| 'savedInputs'
| 'json'
| 'captures'
| 'firstStepInputs'
| undefined
previewSchema?: Record<string, any> | undefined
editPanelInitialSize?: number | undefined
editPanelSize?: number
diff?: Record<string, SchemaDiff>
disableDnd?: boolean
shouldDispatchChanges?: boolean
isValid?: boolean
customUi?: EditableSchemaFormUi | undefined
pannelExtraButtonWidth?: number
class?: string
openEditTab?: import('svelte').Snippet
addProperty?: import('svelte').Snippet
runButton?: import('svelte').Snippet
extraTab?: import('svelte').Snippet
}
let {
schema = $bindable(),
hiddenArgs = [],
args = $bindable(undefined),
shouldHideNoInputs = false,
noVariablePicker = false,
flexWrap = false,
uiOnly = false,
isFlowInput = false,
noPreview = false,
jsonEnabled = true,
isAppInput = false,
displayWebhookWarning = false,
onlyMaskPassword = false,
dndType = undefined,
editTab,
previewSchema = undefined,
editPanelInitialSize = undefined,
editPanelSize = $bindable(0),
diff = {},
disableDnd = false,
shouldDispatchChanges = false,
isValid = $bindable(true),
customUi = undefined,
pannelExtraButtonWidth = 0,
class: clazz = '',
openEditTab,
addProperty,
runButton,
extraTab
}: Props = $props()
$effect.pre(() => {
if (args == undefined) {
args = {}
}
})
export function setDefaults() {
const nargs = {}
Object.keys(schema?.properties ?? {}).forEach((key) => {
if (schema?.properties[key].default != undefined && args[key] == undefined) {
if (schema?.properties[key].default != undefined && args?.[key] == undefined) {
let value = schema?.properties[key].default
nargs[key] = value === 'object' ? structuredClone(value) : value
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let itemPicker: ItemPicker | undefined = $state(undefined)
let variableEditor: VariableEditor | undefined = $state(undefined)
let keys: string[] = Array.isArray(schema?.order)
? [...schema.order]
: (Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {}))
$: schema && onSchemaChange()
let keys: string[] = $state(
Array.isArray(schema?.order)
? [...schema.order]
: (Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {}))
)
function alignOrderWithProperties(schema: {
properties: Record<string, any>
@@ -134,15 +177,25 @@
}
}
let opened: string | undefined = keys[0]
let selected = ''
let opened: string | undefined = $state(untrack(() => keys[0]))
function computeSelected(property: any) {
if (!opened) return ''
return property.type !== 'object'
? property.type
: property.format === 'resource-s3_object'
? 'S3'
: property.oneOf && property.oneOf.length >= 2
? 'oneOf'
: 'object'
}
export function openField(key: string) {
opened = key
}
export function deleteField(key: string) {
delete args[key]
delete args?.[key]
delete schema.properties[key]
if (schema.required?.includes(key)) {
schema.required = schema.required?.filter((x) => x !== key)
@@ -152,20 +205,6 @@
}
}
$: opened && updateSelected(schema.properties[opened])
function updateSelected(property: any) {
if (!property) return
selected = opened
? property.type !== 'object'
? property.type
: property.format === 'resource-s3_object'
? 'S3'
: property.oneOf && property.oneOf.length >= 2
? 'oneOf'
: 'object'
: ''
}
function renameProperty(oldName: string, key: string) {
const el = document.getElementById(key) as HTMLInputElement
const newName = el.value
@@ -179,8 +218,10 @@
// clear the input
el.value = oldName
} else {
args[newName] = args[oldName]
delete args[oldName]
if (args) {
args[newName] = args[oldName]
delete args[oldName]
}
schema.properties[newName] = schema.properties[oldName]
delete schema.properties[oldName]
@@ -199,26 +240,34 @@
opened = newName
schema = schema
schema = $state.snapshot(schema)
dispatch('change', schema)
sendUserToast('Argument renamed successfully')
sendUserToast('Argument renamed')
}
}
let jsonView: boolean = customUi?.jsonOnly == true
let schemaString: string = JSON.stringify(schema, null, '\t')
let error: string | undefined = undefined
let editor: SimpleEditor | undefined = undefined
let jsonView: boolean = $state(customUi?.jsonOnly == true)
let schemaString: string = $state(JSON.stringify(schema, null, '\t'))
let error: string | undefined = $state(undefined)
let editor: SimpleEditor | undefined = $state(undefined)
export function updateJson() {
schemaString = JSON.stringify(schema, null, '\t')
editor?.setCode(schemaString)
}
const editTabDefaultSize = noPreview ? 100 : 50
editPanelSize = editTab ? (editPanelInitialSize ?? editTabDefaultSize) : 0
let inputPanelSize = 100 - editPanelSize
let inputPanelSize = $state(100 - editPanelSize)
let editPanelSizeSmooth = tweened(editPanelSize, {
duration: 150
})
let inputPanelSizeSmooth = tweened(inputPanelSize, { duration: 150 })
let inputPanelSizeSmooth = tweened(
untrack(() => inputPanelSize),
{ duration: 150 }
)
function openEditTab() {
function openEditTabFn() {
if (editPanelSize > 0) return
editPanelSizeSmooth.set(editTabDefaultSize)
inputPanelSizeSmooth.set(100 - editTabDefaultSize)
@@ -234,17 +283,22 @@
inputPanelSize = inputSize
dispatchIfMounted('editPanelSizeChanged', editSize)
}
$: updatePanelSizes($editPanelSizeSmooth, $inputPanelSizeSmooth)
$: !!editTab ? openEditTab() : closeEditTab()
let panelButtonWidth: number = 0
export let pannelExtraButtonWidth: number = 0
export function updateJson() {
schemaString = JSON.stringify(schema, null, '\t')
editor?.setCode(schemaString)
}
let panelButtonWidth: number = $state(0)
$effect(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
$effect(() => {
schema && untrack(() => onSchemaChange())
})
$effect(() => {
updatePanelSizes($editPanelSizeSmooth, $inputPanelSizeSmooth)
})
$effect(() => {
!!editTab ? openEditTabFn() : closeEditTab()
})
</script>
<div class="w-full h-full">
@@ -254,37 +308,42 @@
style="right: calc({editPanelSize}% - 1px - {pannelExtraButtonWidth}px); top: 0px;"
bind:clientWidth={panelButtonWidth}
>
<slot name="openEditTab" />
{@render openEditTab?.()}
</div>
</div>
<Splitpanes class="splitter-hidden w-full">
{#if !noPreview}
<Pane bind:size={inputPanelSize} minSize={20}>
<div
class="h-full flex flex-col gap-2 {$$slots.openEditTab && editPanelSize > 0
class="h-full flex flex-col gap-2 {openEditTab && editPanelSize > 0
? 'pr-[38px]'
: 'pr-2'}"
>
{#if $$slots.addProperty}
{#if addProperty}
<div class="w-full justify-left pr-2 grow-0">
<div
style={editPanelSize > 0
? `width: 100%;`
: `width: calc(100% - ${panelButtonWidth - pannelExtraButtonWidth}px);`}
>
<slot name="addProperty" />
{@render addProperty?.()}
</div>
</div>
{/if}
<div
class="min-h-0 overflow-y-auto grow rounded-md {$$slots.runButton
? 'flex flex-col gap-2'
: ''}"
class="min-h-0 overflow-y-auto grow rounded-md {runButton ? 'flex flex-col gap-2' : ''}"
>
<SchemaFormDnd
nestedClasses={'flex flex-col gap-1'}
schema={previewSchema ? previewSchema : schema}
bind:schema={
() => (previewSchema ? previewSchema : schema),
(newSchema) => {
console.log('schemaChange set', $state.snapshot(newSchema))
schema = newSchema
tick().then(() => dispatch('change', schema))
}
}
{dndType}
{disableDnd}
{onlyMaskPassword}
@@ -293,12 +352,10 @@
opened = e.detail
}}
on:reorder={(e) => {
schema.order = e.detail
schema = schema
tick().then(() => dispatch('change', schema))
}}
on:change={() => {
schema = schema
schema = {
...schema,
order: e.detail
}
tick().then(() => dispatch('change', schema))
}}
prettifyHeader={isAppInput}
@@ -314,7 +371,7 @@
noVariablePicker={noVariablePicker || customUi?.disableVariablePicker === true}
/>
<slot name="runButton" />
{@render runButton?.()}
</div>
</div>
</Pane>
@@ -327,7 +384,7 @@
class={twMerge('border rounded-md', panelButtonWidth > 0 ? 'rounded-tl-none' : '')}
>
{#if editTab !== 'inputEditor'}
<slot name="extraTab" />
{@render extraTab?.()}
{:else}
<!-- WIP -->
{#if jsonEnabled && customUi?.jsonOnly != true}
@@ -357,14 +414,14 @@
{#if keys.length > 0}
{#each keys as argName, i (argName)}
<div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={twMerge(
'w-full flex bg-gray-50 dark:bg-gray-800 px-4 py-1 justify-between items-center hover:bg-gray-100 cursor-pointer',
opened === argName ? 'bg-gray-100 hover:bg-gray-200' : ''
)}
on:click={() => {
onclick={() => {
if (opened === argName) {
opened = undefined
} else {
@@ -375,9 +432,9 @@
<div class="flex flex-row gap-2">
{argName}
{#if !uiOnly}
<div on:click|stopPropagation|preventDefault>
<div onclick={stopPropagation(preventDefault(bubble('click')))}>
<Popover placement="bottom-end" containerClasses="p-4" closeButton>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
color="light"
size="xs2"
@@ -385,8 +442,8 @@
startIcon={{ icon: Pen }}
iconOnly
/>
</svelte:fragment>
<svelte:fragment slot="content" let:close>
{/snippet}
{#snippet content({ close })}
<Label label="Name" class="p-4">
<div class="flex flex-col gap-2">
<input
@@ -394,7 +451,7 @@
class="w-full !bg-surface"
value={argName}
id={argName + i}
on:keydown={(event) => {
onkeydown={(event) => {
if (event.key === 'Enter') {
renameProperty(argName, argName + i)
close()
@@ -414,7 +471,7 @@
</Button>
</div>
</Label>
</svelte:fragment>
{/snippet}
</Popover>
</div>
{/if}
@@ -429,7 +486,7 @@
class="rounded-full p-1 text-gray-500 bg-white
duration-200 hover:bg-gray-600 focus:bg-gray-600 hover:text-white dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800"
aria-label="Clear"
on:click={() => {
onclick={() => {
dispatch('delete', argName)
}}
>
@@ -461,103 +518,103 @@
{isFlowInput}
{isAppInput}
on:change={() => {
schema = schema
schema = $state.snapshot(schema)
dispatch('change', schema)
}}
>
<svelte:fragment slot="typeeditor">
{#snippet typeeditor()}
{#if isFlowInput || isAppInput}
<Label label="Type">
<ToggleButtonGroup
tabListClass="flex-wrap"
class="h-auto"
let:item
bind:selected
bind:selected={
() => computeSelected(schema.properties[opened ?? '']),
(v) => {
const isS3 = v == 'S3'
const isOneOf = v == 'oneOf'
const emptyProperty = {
contentEncoding: undefined,
enum_: undefined,
pattern: undefined,
default: undefined,
min: undefined,
max: undefined,
currency: undefined,
currencyLocale: undefined,
multiselect: undefined,
password: undefined,
dateFormat: undefined,
...(v == 'array' ? { items: { type: 'string' } } : {}),
showExpr: undefined,
nullable: undefined,
required: undefined
}
if (isS3) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
format: 'resource-s3_object'
}
} else if (isOneOf) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
oneOf: [
{
title: 'Option 1',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 1']
},
property_1: {
type: 'string'
}
}
},
{
title: 'Option 2',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 2']
},
property_2: {
type: 'string'
}
}
}
]
}
} else {
schema.properties[argName] = {
...emptyProperty,
format: undefined,
type: v
}
}
}
}
on:selected={(e) => {
const isS3 = e.detail == 'S3'
const isOneOf = e.detail == 'oneOf'
const emptyProperty = {
contentEncoding: undefined,
enum_: undefined,
pattern: undefined,
default: undefined,
min: undefined,
max: undefined,
currency: undefined,
currencyLocale: undefined,
multiselect: undefined,
password: undefined,
dateFormat: undefined,
...(e.detail == 'array'
? { items: { type: 'string' } }
: {}),
showExpr: undefined,
nullable: undefined,
required: undefined
}
if (isS3) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
format: 'resource-s3_object'
}
} else if (isOneOf) {
schema.properties[argName] = {
...emptyProperty,
type: 'object',
oneOf: [
{
title: 'Option 1',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 1']
},
property_1: {
type: 'string'
}
}
},
{
title: 'Option 2',
type: 'object',
properties: {
label: {
type: 'string',
enum: ['Option 2']
},
property_2: {
type: 'string'
}
}
}
]
}
} else {
schema.properties[argName] = {
...emptyProperty,
format: undefined,
type: e.detail
}
}
// No better solution than this, needs future rework
setTimeout(() => {
schema = schema
dispatch('change', schema)
}, 100)
schema = schema
dispatch('change', schema)
dispatch('schemaChange')
}}
>
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
<ToggleButton value={x[1]} label={x[0]} {item} />
{/each}
{#snippet children({ item })}
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
<ToggleButton value={x[1]} label={x[0]} {item} />
{/each}
{/snippet}
</ToggleButtonGroup>
</Label>
{/if}
</svelte:fragment>
{/snippet}
{#if isFlowInput || isAppInput}
<FlowPropertyEditor
@@ -594,7 +651,7 @@
dispatch('change', schema)
}}
on:schemaChange={(e) => {
schema = schema
schema = $state.snapshot(schema)
dispatch('change', schema)
dispatch('schemaChange')
}}
@@ -651,7 +708,9 @@
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
args[pickForField] = '$var:' + path
if (args) {
args[pickForField] = '$var:' + path
}
}
}}
itemName="Variable"
@@ -664,17 +723,19 @@
...x
}))}
>
<div slot="submission">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{#snippet submission()}
<div>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
+15 -10
View File
@@ -206,7 +206,7 @@
let websocketInterval: NodeJS.Timeout | undefined
let lastWsAttempt: Date = new Date()
let nbWsAttempt = 0
let disposeMethod: () => void | undefined
let disposeMethod: (() => void) | undefined
const dispatch = createEventDispatcher()
let graphqlService: MonacoGraphQLAPI | undefined = undefined
@@ -1267,15 +1267,20 @@
onFileChanges()
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
fontSize: !small ? 14 : 12,
lineNumbersMinChars,
// overflowWidgetsDomNode: widgets,
tabSize: lang == 'python' ? 4 : 2,
folding
})
try {
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
fontSize: !small ? 14 : 12,
lineNumbersMinChars,
// overflowWidgetsDomNode: widgets,
tabSize: lang == 'python' ? 4 : 2,
folding
})
} catch (e) {
console.error('Error loading monaco:', e)
return
}
keepModelAroundToAvoidDisposalOfWorkers()
+174 -129
View File
@@ -1,8 +1,10 @@
<script context="module" lang="ts">
<script module lang="ts">
export const EDITOR_BAR_WIDTH_THRESHOLD = 1044
</script>
<script lang="ts">
import { run } from 'svelte/legacy'
import { ResourceService, VariableService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -19,7 +21,7 @@
import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import { SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS } from '$lib/consts'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { sendUserToast } from '$lib/toast'
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
import Toggle from './Toggle.svelte'
@@ -47,110 +49,145 @@
import type { EditorBarUi } from './custom_ui'
import EditorSettings from './EditorSettings.svelte'
export let lang: SupportedLanguage | 'bunnative' | undefined
export let editor: Editor | undefined
export let websocketAlive: {
pyright: boolean
ruff: boolean
deno: boolean
go: boolean
shellcheck: boolean
interface Props {
lang: SupportedLanguage | 'bunnative' | undefined
editor: Editor | undefined
websocketAlive: {
pyright: boolean
ruff: boolean
deno: boolean
go: boolean
shellcheck: boolean
}
iconOnly?: boolean
validCode?: boolean
kind?: 'script' | 'trigger' | 'approval'
template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative'
collabMode?: boolean
collabLive?: boolean
collabUsers?: { name: string }[]
scriptPath?: string | undefined
diffEditor?: DiffEditor | undefined
args: Record<string, any>
noHistory?: boolean
saveToWorkspace?: boolean
customUi?: EditorBarUi
lastDeployedCode?: string | undefined
diffMode?: boolean
showHistoryDrawer?: boolean
right?: import('svelte').Snippet
}
export let iconOnly: boolean = false
export let validCode: boolean = true
export let kind: 'script' | 'trigger' | 'approval' = 'script'
export let template: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' =
'script'
export let collabMode = false
export let collabLive = false
export let collabUsers: { name: string }[] = []
export let scriptPath: string | undefined = undefined
export let diffEditor: DiffEditor | undefined = undefined
export let args: Record<string, any>
export let noHistory = false
export let saveToWorkspace = false
export let customUi: EditorBarUi = {}
export let lastDeployedCode: string | undefined = undefined
export let diffMode: boolean = false
export let showHistoryDrawer: boolean = false
let contextualVariablePicker: ItemPicker
let variablePicker: ItemPicker
let resourcePicker: ItemPicker
let resourceTypePicker: ItemPicker
let variableEditor: VariableEditor
let resourceEditor: ResourceEditorDrawer
let showContextVarPicker = false
let showVarPicker = false
let showResourcePicker = false
let showResourceTypePicker = false
let {
lang,
editor,
websocketAlive,
iconOnly = false,
validCode = true,
kind = 'script',
template = 'script',
collabMode = false,
collabLive = false,
collabUsers = [],
scriptPath = undefined,
diffEditor = undefined,
args,
noHistory = false,
saveToWorkspace = false,
customUi = {},
lastDeployedCode = undefined,
diffMode = false,
showHistoryDrawer = $bindable(false),
right
}: Props = $props()
$: showContextVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
$: showVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
$: showResourcePicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
$: showResourceTypePicker =
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) ||
lang === 'python3' ||
lang === 'php'
let contextualVariablePicker: ItemPicker | undefined = $state()
let variablePicker: ItemPicker | undefined = $state()
let resourcePicker: ItemPicker | undefined = $state()
let resourceTypePicker: ItemPicker | undefined = $state()
let variableEditor: VariableEditor | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
let showContextVarPicker = $state(false)
let showVarPicker = $state(false)
let showResourcePicker = $state(false)
let showResourceTypePicker = $state(false)
let codeViewer: Drawer
let codeObj: { language: SupportedLanguage; content: string } | undefined = undefined
run(() => {
showContextVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
})
run(() => {
showVarPicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
})
run(() => {
showResourcePicker = [
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
})
run(() => {
showResourceTypePicker =
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) ||
lang === 'python3' ||
lang === 'php'
})
let codeViewer: Drawer | undefined = $state()
let codeObj: { language: SupportedLanguage; content: string } | undefined = $state(undefined)
function addEditorActions() {
editor?.addAction('insert-variable', 'Windmill: Insert variable', () => {
variablePicker.openDrawer()
variablePicker?.openDrawer()
})
editor?.addAction('insert-resource', 'Windmill: Insert resource', () => {
resourcePicker.openDrawer()
resourcePicker?.openDrawer()
})
}
$: editor && addEditorActions()
run(() => {
editor && untrack(() => addEditorActions())
})
async function loadVariables() {
return await VariableService.listVariable({ workspace: $workspaceStore ?? '' })
@@ -162,9 +199,9 @@
})
}
let scriptPicker: Drawer
let pick_existing: 'hub' | 'workspace' = 'hub'
let filter = ''
let scriptPicker: Drawer | undefined = $state()
let pick_existing: 'hub' | 'workspace' = $state('hub')
let filter = $state('')
async function onScriptPick(e: { detail: { path: string } }) {
codeObj = undefined
@@ -462,21 +499,23 @@ string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
itemName="Variable"
extraField="path"
loadItems={loadVariables}
buttons={{ 'Edit/View': (x) => variableEditor.editVariable(x) }}
buttons={{ 'Edit/View': (x) => variableEditor?.editVariable(x) }}
>
<div slot="submission" class="flex flex-row">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor.initNew()
}}
>
New variable
</Button>
</div>
{#snippet submission()}
<div class="flex flex-row">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew()
}}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<ItemPicker
@@ -548,24 +587,26 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
tooltip="Resources represent connections to third party systems. Resources are a good way to define a connection to a frequently used third party system such as a database."
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
itemName="Resource"
buttons={{ 'Edit/View': (x) => resourceEditor.initEdit(x) }}
buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }}
extraField="description"
extraField2="resource_type"
loadItems={async () =>
await ResourceService.listResource({ workspace: $workspaceStore ?? 'NO_W' })}
>
<div slot="submission" class="flex flex-row gap-x-1 mr-2">
<Button
startIcon={{ icon: Plus }}
target="_blank"
variant="border"
color="blue"
size="sm"
href="{base}/resources?connect_app=undefined"
>
Add resource
</Button>
</div>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
<Button
startIcon={{ icon: Plus }}
target="_blank"
variant="border"
color="blue"
size="sm"
href="{base}/resources?connect_app=undefined"
>
Add resource
</Button>
</div>
{/snippet}
</ItemPicker>
{#if showResourceTypePicker}
@@ -650,7 +691,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
size="xs"
spacingSize="md"
color="light"
on:click={resourceTypePicker.openDrawer}
on:click={() => resourceTypePicker?.openDrawer()}
{iconOnly}
startIcon={{ icon: Package }}
>
@@ -724,7 +765,9 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
}}
/>
<Popover>
<svelte:fragment slot="text">Toggle diff mode</svelte:fragment>
{#snippet text()}
Toggle diff mode
{/snippet}
<DiffIcon class="ml-1 text-tertiary" size={14} />
</Popover>
</div>
@@ -739,14 +782,16 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
on:change={() => dispatch('toggleCollabMode')}
/>
<Popover>
<svelte:fragment slot="text">Multiplayer</svelte:fragment>
{#snippet text()}
Multiplayer
{/snippet}
<Users class="ml-1 text-tertiary" size={14} />
</Popover>
{#if collabLive}
<button
title="Show invite link"
class="p-1 rounded hover:bg-gray-400 mx-1 border"
on:click={() => dispatch('collabPopup')}><Link size={14} /></button
onclick={() => dispatch('collabPopup')}><Link size={14} /></button
>
<div class="isolate flex -space-x-2 pl-2">
{#each collabUsers as user}
@@ -773,7 +818,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
</div>
<div class="flex flex-row items-center gap-2">
<slot name="right" />
{@render right?.()}
{#if scriptPath && !noHistory}
<Button
btnClasses="!font-medium text-tertiary"
@@ -329,7 +329,7 @@
schema={customHandlerSchema}
bind:args={handlerExtraArgs}
shouldHideNoInputs
class="text-xs"
className="text-xs"
/>
{/await}
{#if customHandlerSchema && customHandlerSchema.properties && Object.keys(customHandlerSchema.properties).length === 0}
@@ -368,7 +368,7 @@
}}
bind:args={handlerExtraArgs}
shouldHideNoInputs
class="text-xs"
className="text-xs"
/>
{/await}
{:else if workspaceConnectedToSlack == undefined}
@@ -1,6 +1,6 @@
<script lang="ts">
import { createEventDispatcher, onDestroy, getContext } from 'svelte'
import { getFirstStepSchema } from '$lib/components/flows/flowStore'
import { createEventDispatcher, onDestroy, getContext, untrack } from 'svelte'
import { getFirstStepSchema } from '$lib/components/flows/flowStore.svelte'
import type { FlowEditorContext } from '$lib/components/flows/types'
import { twMerge } from 'tailwind-merge'
import { Alert } from '$lib/components/common'
@@ -13,13 +13,13 @@
const { flowStore, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
let schema: Record<string, any> | undefined = undefined
let schema: Record<string, any> | undefined = $state(undefined)
let error: string | undefined = undefined
let mod: any | undefined = undefined
let error: string | undefined = $state(undefined)
let mod: any | undefined = $state(undefined)
async function loadSchema() {
try {
const res = await getFirstStepSchema($flowStateStore, flowStore)
const res = await getFirstStepSchema($flowStateStore, flowStore.val)
schema = res.schema
mod = res.mod
dispatch('connectFirstNode', { connectFirstNode: res.connectFirstNode })
@@ -27,7 +27,9 @@
error = e
}
}
$: $flowStore && $flowStateStore && loadSchema()
$effect(() => {
flowStore.val && $flowStateStore && untrack(() => loadSchema())
})
function handleClick() {
selected = !selected
@@ -39,7 +41,7 @@
dispatch('select', undefined)
})
let selected: boolean = false
let selected: boolean = $state(false)
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && selected) {
@@ -50,14 +52,16 @@
}
}
let firstUpdate = true
$: if (schema && !selected && firstUpdate) {
firstUpdate = false
setTimeout(() => {
selected = true
dispatch('select', schema)
}, 200)
}
let firstUpdate = $state(true)
$effect(() => {
if (schema && !selected && firstUpdate) {
firstUpdate = false
setTimeout(() => {
selected = true
dispatch('select', schema)
}, 200)
}
})
export function resetSelected(dispatchEvent?: boolean) {
selected = false
@@ -67,7 +71,7 @@
}
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window onkeydown={handleKeydown} />
<div class="h-full">
{#if schema && mod}
@@ -77,10 +81,10 @@
selected ? 'bg-surface-selected' : 'hover:bg-surface-hover'
)}
disabled={!schema}
on:click={handleClick}
onclick={handleClick}
>
<FlowModuleSchemaItemViewer
on:click={handleClick}
onclick={handleClick}
deletable={false}
id={mod.id}
label={mod.summary ||
@@ -90,24 +94,26 @@
: 'To be defined')}
path={`path` in mod.value && mod.summary ? mod.value.path : ''}
>
<div slot="icon">
{#if mod.value.type === 'rawscript'}
<LanguageIcon lang={mod.value.language} width={16} height={16} />
{:else if mod.value.type === 'script'}
{#if mod.value.path.startsWith('hub/')}
<div>
<IconedResourceType
width="20px"
height="20px"
name={mod.value.path.split('/')[2]}
silent={true}
/>
</div>
{:else}
<Building size={14} />
{#snippet icon()}
<div>
{#if mod.value.type === 'rawscript'}
<LanguageIcon lang={mod.value.language} width={16} height={16} />
{:else if mod.value.type === 'script'}
{#if mod.value.path.startsWith('hub/')}
<div>
<IconedResourceType
width="20px"
height="20px"
name={mod.value.path.split('/')[2]}
silent={true}
/>
</div>
{:else}
<Building size={14} />
{/if}
{/if}
{/if}
</div>
</div>
{/snippet}
</FlowModuleSchemaItemViewer>
</button>
{:else}
+140 -95
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import {
FlowService,
type Flow,
type FlowModule,
DraftService,
type PathScript,
type OpenFlow,
@@ -23,14 +24,16 @@
encodeState,
generateRandomString,
orderedJsonStringify,
readFieldsRecursively,
replaceFalseWithUndefined,
type StateStore,
type Value
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import { onMount, setContext, type ComponentType } from 'svelte'
import { onMount, setContext, untrack, type ComponentType } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Badge, Button, UndoRedo } from './common'
@@ -48,7 +51,7 @@
import { getAllModules } from './flows/flowExplorer'
import { type FlowCopilotContext } from './copilot/flow'
import FlowAIButton from './copilot/chat/flow/FlowAIButton.svelte'
import { loadFlowModuleState } from './flows/flowStateUtils'
import { loadFlowModuleState } from './flows/flowStateUtils.svelte'
import FlowBuilderTutorials from './FlowBuilderTutorials.svelte'
import Dropdown from '$lib/components/DropdownV2.svelte'
import FlowTutorials from './FlowTutorials.svelte'
@@ -71,27 +74,51 @@
import { Triggers } from './triggers/triggers.svelte'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
export let initialPath: string = ''
export let pathStoreInit: string | undefined = undefined
export let newFlow: boolean
export let selectedId: string | undefined
export let initialArgs: Record<string, any> = {}
export let loading = false
export let flowStore: Writable<OpenFlow>
export let flowStateStore: Writable<FlowState>
export let savedFlow: FlowWithDraftAndDraftTriggers | undefined = undefined
export let diffDrawer: DiffDrawer | undefined = undefined
export let customUi: FlowBuilderWhitelabelCustomUi = {}
export let disableAi: boolean = false
export let disabledFlowInputs = false
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined // used to set the primary schedule in the legacy primaryScheduleStore
export let version: number | undefined = undefined
export let setSavedraftCb: ((cb: () => void) => void) | undefined = undefined
export let draftTriggersFromUrl: Trigger[] | undefined = undefined
export let selectedTriggerIndexFromUrl: number | undefined = undefined
interface Props {
initialPath?: string
pathStoreInit?: string | undefined
newFlow: boolean
selectedId: string | undefined
initialArgs?: Record<string, any>
loading?: boolean
flowStore: StateStore<OpenFlow>
flowStateStore: Writable<FlowState>
savedFlow?: FlowWithDraftAndDraftTriggers | undefined
diffDrawer?: DiffDrawer | undefined
customUi?: FlowBuilderWhitelabelCustomUi
disableAi?: boolean
disabledFlowInputs?: boolean
savedPrimarySchedule?: ScheduleTrigger | undefined // used to set the primary schedule in the legacy primaryScheduleStore
version?: number | undefined
setSavedraftCb?: ((cb: () => void) => void) | undefined
draftTriggersFromUrl?: Trigger[] | undefined
selectedTriggerIndexFromUrl?: number | undefined
children?: import('svelte').Snippet
}
let {
initialPath = $bindable(''),
pathStoreInit = undefined,
newFlow,
selectedId,
initialArgs = {},
loading = false,
flowStore,
flowStateStore,
savedFlow = $bindable(undefined),
diffDrawer = undefined,
customUi = {},
disableAi = false,
disabledFlowInputs = false,
savedPrimarySchedule = undefined,
version = undefined,
setSavedraftCb = undefined,
draftTriggersFromUrl = undefined,
selectedTriggerIndexFromUrl = undefined,
children
}: Props = $props()
let initialPathStore = writable(initialPath)
$: initialPathStore.set(initialPath)
// used for new flows for captures
let fakeInitialPath =
@@ -103,13 +130,13 @@
generateRandomString(12)
// Used by multiplayer deploy collision warning
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
let deployedValue: Value | undefined = $state(undefined) // Value to diff against
let deployedBy: string | undefined = $state(undefined) // Author
let confirmCallback: () => void = $state(() => {}) // What happens when user clicks `override` in warning
let open: boolean = $state(false) // Is confirmation modal open
// Draft triggers confirmation modal
let draftTriggersModalOpen = false
let draftTriggersModalOpen = $state(false)
let confirmDeploymentCallback: (triggersToDeploy: Trigger[]) => void = () => {}
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
@@ -119,13 +146,11 @@
confirmDeploymentCallback(selectedTriggers)
}
$: setContext('customUi', customUi)
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
savedValue: savedFlow,
modifiedValue: {
...$flowStore,
...flowStore.val,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
}
}
@@ -175,8 +200,8 @@
triggersState.selectedTriggerIndex = index
}
let loadingSave = false
let loadingDraft = false
let loadingSave = $state(false)
let loadingDraft = $state(false)
export async function saveDraft(forceSave = false): Promise<void> {
if (!newFlow && !savedFlow) {
@@ -185,11 +210,13 @@
if (savedFlow) {
const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow)
const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
const current = cleanValueProperties({
...$flowStore,
path: $pathStore,
draft_triggers: currentDraftTriggers
})
const current = cleanValueProperties(
$state.snapshot({
...flowStore.val,
path: $pathStore,
draft_triggers: currentDraftTriggers
})
)
if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
sendUserToast('No changes detected, ignoring', false, [
{
@@ -204,7 +231,7 @@
}
loadingDraft = true
try {
const flow = cleanInputs($flowStore)
const flow = cleanInputs(flowStore.val)
try {
localStorage.removeItem('flow')
localStorage.removeItem(`flow-${$pathStore}`)
@@ -261,13 +288,13 @@
savedFlow = {
...(newFlow || savedFlow?.draft_only
? {
...structuredClone($flowStore),
...structuredClone($state.snapshot(flowStore.val)),
path: $pathStore,
draft_only: true
}
: savedFlow),
draft: {
...structuredClone($flowStore),
...structuredClone($state.snapshot(flowStore.val)),
path: $pathStore,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
}
@@ -314,9 +341,9 @@
if (
deployedValue &&
$flowStore &&
flowStore.val &&
orderedJsonStringify(deployedValue) ===
orderedJsonStringify(replaceFalseWithUndefined({ ...$flowStore, path: $pathStore }))
orderedJsonStringify(replaceFalseWithUndefined({ ...flowStore.val, path: $pathStore }))
) {
await saveFlow(deploymentMsg)
} else {
@@ -359,7 +386,7 @@
loadingSave = true
try {
const flow = cleanInputs($flowStore)
const flow = cleanInputs(flowStore.val)
// console.log('flow', computeUnlockedSteps(flow)) // del
// loadingSave = false // del
// return
@@ -441,11 +468,11 @@
})
}
const { draft_triggers: _, ...newSavedFlow } = $flowStore as OpenFlow & {
const { draft_triggers: _, ...newSavedFlow } = flowStore.val as OpenFlow & {
draft_triggers: Trigger[]
}
savedFlow = {
...structuredClone(newSavedFlow),
...structuredClone($state.snapshot(newSavedFlow)),
path: $pathStore
} as Flow
setDraftTriggers([])
@@ -453,19 +480,13 @@
dispatch('deploy', $pathStore)
} catch (err) {
dispatch('deployError', err)
sendUserToast(`The flow could not be saved: ${err.body}`, true)
sendUserToast(`The flow could not be saved: ${err.body ?? err}`, true)
loadingSave = false
}
}
let timeout: NodeJS.Timeout | undefined = undefined
$: {
if ($flowStore || $selectedIdStore) {
saveSessionDraft()
}
}
function saveSessionDraft() {
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
@@ -473,7 +494,7 @@
localStorage.setItem(
initialPath && initialPath != '' ? `flow-${initialPath}` : 'flow',
encodeState({
flow: $flowStore,
flow: flowStore.val,
path: $pathStore,
selectedId: $selectedIdStore,
draft_triggers: triggersState.getDraftTriggersSnapshot(),
@@ -492,10 +513,10 @@
return $selectedIdStore
}
const previewArgsStore = writable<Record<string, any>>(initialArgs)
const previewArgsStore = $state({ val: initialArgs })
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
const history = initHistory($flowStore)
const moving = writable<{ id: string } | undefined>(undefined)
const history = initHistory(flowStore.val)
const pathStore = writable<string>(pathStoreInit ?? initialPath)
const captureOn = writable<boolean>(false)
const showCaptureHint = writable<boolean | undefined>(undefined)
@@ -504,7 +525,6 @@
editPanelSize: 0,
payloadData: undefined
})
$: initialPath && ($pathStore = initialPath)
const testStepStore = writable<Record<string, any>>({})
@@ -536,14 +556,16 @@
})
// Add triggers context store
const triggersState = new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'email', path: '', isDraft: false },
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
],
selectedTriggerIndexFromUrl,
saveSessionDraft
const triggersState = $state(
new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'email', path: '', isDraft: false },
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
],
selectedTriggerIndexFromUrl,
saveSessionDraft
)
)
setContext<TriggerContext>('TriggerContext', {
@@ -575,10 +597,6 @@
}
}
$: selectedId && select(selectedId)
$: initialPath && initialPath != '' && $workspaceStore && loadTriggers()
function onKeyDown(event: KeyboardEvent) {
let classes = event.target?.['className']
if (
@@ -591,13 +609,13 @@
switch (event.key) {
case 'Z':
if (event.ctrlKey || event.metaKey) {
$flowStore = redo(history)
flowStore.val = redo(history)
event.preventDefault()
}
break
case 'z':
if (event.ctrlKey || event.metaKey) {
$flowStore = undo(history, $flowStore)
flowStore.val = undo(history, flowStore.val)
$selectedIdStore = 'Input'
event.preventDefault()
}
@@ -638,7 +656,7 @@
'settings-metadata',
'constants',
'preprocessor',
...dfsApply($flowStore.value.modules, (module) => module.id)
...dfsApply(flowStore.val.value.modules, (module) => module.id)
]
}
@@ -663,7 +681,7 @@
}
}
let flowCopilotContext: FlowCopilotContext = {
let flowCopilotContext: FlowCopilotContext = $state({
shouldUpdatePropertyType: writable<{
[key: string]: 'static' | 'javascript' | undefined
}>({}),
@@ -674,16 +692,16 @@
[key: string]: string | undefined
}>({}),
stepInputsLoading: writable<boolean>(false)
}
})
setContext('FlowCopilotContext', flowCopilotContext)
let renderCount = 0
let flowTutorials: FlowTutorials | undefined = undefined
let renderCount = $state(0)
let flowTutorials: FlowTutorials | undefined = $state(undefined)
let jsonViewerDrawer: Drawer | undefined = undefined
let yamlEditorDrawer: Drawer | undefined = undefined
let flowHistory: FlowHistory | undefined = undefined
let jsonViewerDrawer: Drawer | undefined = $state(undefined)
let yamlEditorDrawer: Drawer | undefined = $state(undefined)
let flowHistory: FlowHistory | undefined = $state(undefined)
export function triggerTutorial() {
const urlParams = new URLSearchParams(window.location.search)
@@ -701,9 +719,7 @@
icon: ComponentType<Icon>
action: () => void
disabled?: boolean
}[] = []
$: onCustomUiChange(customUi)
}[] = $state([])
function onCustomUiChange(customUi: FlowBuilderWhitelabelCustomUi | undefined) {
moreItems = [
@@ -748,12 +764,37 @@
}
}
let flowPreviewButtons: FlowPreviewButtons
let flowPreviewButtons: FlowPreviewButtons | undefined = $state()
run(() => {
initialPathStore.set(initialPath)
})
run(() => {
setContext('customUi', customUi)
})
run(() => {
if (flowStore.val || $selectedIdStore) {
readFieldsRecursively(flowStore.val)
untrack(() => saveSessionDraft())
}
})
run(() => {
initialPath && ($pathStore = initialPath)
})
run(() => {
selectedId && untrack(() => select(selectedId))
})
run(() => {
initialPath && initialPath != '' && $workspaceStore && untrack(() => loadTriggers())
})
run(() => {
customUi && untrack(() => onCustomUiChange(customUi))
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<slot />
{@render children?.()}
<DeployOverrideConfirmationModal
bind:deployedBy
@@ -761,7 +802,7 @@
bind:open
{diffDrawer}
bind:deployedValue
currentValue={$flowStore}
currentValue={flowStore.val}
/>
<DraftTriggersConfirmationModal
@@ -791,18 +832,18 @@
<div class="flex w-full max-w-md gap-4 items-center">
<Summary
disabled={customUi?.topBar?.editableSummary == false}
bind:value={$flowStore.summary}
bind:value={flowStore.val.summary}
/>
<UndoRedo
undoProps={{ disabled: $history.index === 0 }}
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
on:undo={() => {
const currentModules = $flowStore?.value?.modules
const currentModules = flowStore.val?.value?.modules
$flowStore = undo(history, $flowStore)
flowStore.val = undo(history, flowStore.val)
const newModules = $flowStore?.value?.modules
const newModules = flowStore.val?.value?.modules
const restoredModules = newModules?.filter(
(node) => !currentModules?.some((currentNode) => currentNode?.id === node?.id)
)
@@ -820,7 +861,7 @@
$selectedIdStore = 'Input'
}}
on:redo={() => {
$flowStore = redo(history)
flowStore.val = redo(history)
}}
/>
</div>
@@ -853,7 +894,7 @@
<div class="flex justify-start w-full">
<div>
<button
on:click={async () => {
onclick={async () => {
select('settings-metadata')
document.getElementById('path')?.focus()
}}
@@ -871,7 +912,7 @@
readonly
value={$pathStore && $pathStore != '' ? $pathStore : 'Choose a path'}
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
onfocus={({ currentTarget }) => {
currentTarget.select()
}}
/>
@@ -915,7 +956,11 @@
mode: 'normal',
deployed: deployedValue ?? savedFlow,
draft: savedFlow?.draft,
current: { ...$flowStore, path: $pathStore, draft_triggers: currentDraftTriggers }
current: {
...flowStore.val,
path: $pathStore,
draft_triggers: currentDraftTriggers
}
})
}}
disabled={!savedFlow}
@@ -980,7 +1025,7 @@
}
}}
on:testWithArgs={(e) => {
$previewArgsStore = JSON.parse(JSON.stringify(e.detail))
previewArgsStore.val = JSON.parse(JSON.stringify(e.detail))
flowPreviewButtons?.openPreview(true)
}}
{savedFlow}
@@ -45,8 +45,7 @@
modules={flow?.value?.modules}
failureModule={flow?.value?.failure_module}
preprocessorModule={flow?.value?.preprocessor_module}
on:select={(e) => {
let nodeId = e?.detail
onSelect={(nodeId) => {
if (nodeId === 'triggers') {
dispatch('triggerDetail')
return
@@ -55,7 +54,7 @@
} else if (nodeId === 'preprocessor') {
stepDetail = flow?.value?.preprocessor_module
} else {
stepDetail = dfs(flow?.value?.modules ?? [], (m) => m).find((m) => m?.id === e?.detail)
stepDetail = dfs(flow?.value?.modules ?? [], (m) => m).find((m) => m?.id === nodeId)
}
stepDetail = stepDetail ?? nodeId
dispatch('select', stepDetail)
@@ -11,13 +11,23 @@
import { CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
import type { Schema } from '$lib/common'
export let open: boolean
interface Props {
open: boolean
jobId?: string | undefined
job?: Job | undefined
modules: FlowModule[]
previewArgs?: Record<string, any>
whileLoop?: boolean
}
export let jobId: string | undefined = undefined
export let job: Job | undefined = undefined
export let modules: FlowModule[]
export let previewArgs: Record<string, any> = {}
export let whileLoop = false
let {
open,
jobId = $bindable(undefined),
job = $bindable(undefined),
modules,
previewArgs = $bindable({}),
whileLoop = false
}: Props = $props()
export const forloopSchema: Schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema' as string | undefined,
@@ -54,10 +64,10 @@
type: 'object'
}
let selectedJobStep: string | undefined = undefined
let selectedJobStep: string | undefined = $state(undefined)
let isRunning: boolean = false
let jobProgressReset: () => void
let isRunning: boolean = $state(false)
let jobProgressReset: (() => void) | undefined = $state(undefined)
export function test() {
runPreview(previewArgs, undefined)
@@ -70,7 +80,7 @@
args: Record<string, any>,
restartedFrom: RestartedFrom | undefined
) {
jobProgressReset()
jobProgressReset?.()
const newFlow = { value: { modules }, summary: '' }
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
@@ -89,12 +99,14 @@
}
}
$: if (job?.type === 'CompletedJob') {
isRunning = false
}
$effect(() => {
if (job?.type === 'CompletedJob') {
isRunning = false
}
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<div class="flex flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full" id="flow-preview-content">
<div class="flex flex-row justify-between w-full items-center gap-x-2">
@@ -156,7 +168,7 @@
<SchemaForm
noVariablePicker
compact
class="py-4 max-w-3xl"
className="py-4 max-w-3xl"
schema={whileLoop ? whileLoopSchema : forloopSchema}
bind:args={previewArgs}
/>
@@ -13,7 +13,7 @@
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
import { emptyString, sendUserToast } from '$lib/utils'
import { dfs } from './flows/dfs'
import { sliceModules } from './flows/flowStateUtils'
import { sliceModules } from './flows/flowStateUtils.svelte'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import Toggle from './Toggle.svelte'
import JsonInputs from './JsonInputs.svelte'
@@ -53,7 +53,7 @@
export function test() {
renderCount++
runPreview($previewArgs, undefined)
runPreview(previewArgs.val, undefined)
}
const {
@@ -75,9 +75,9 @@
function extractFlow(previewMode: 'upTo' | 'whole'): OpenFlow {
if (previewMode === 'whole') {
return $flowStore
return flowStore.val
} else {
const flow: Flow = JSON.parse(JSON.stringify($flowStore))
const flow: Flow = JSON.parse(JSON.stringify(flowStore.val))
const idOrders = dfs(flow.value.modules, (x) => x.id)
let upToIndex = idOrders.indexOf($selectedId)
@@ -97,13 +97,13 @@
initial = false
}
try {
lastPreviewFlow = JSON.stringify($flowStore)
lastPreviewFlow = JSON.stringify(flowStore.val)
jobProgressReset()
const newFlow = extractFlow(previewMode)
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
if (inputSelected) {
savedArgs = $previewArgs
savedArgs = previewArgs.val
inputSelected = undefined
}
} catch (e) {
@@ -120,7 +120,7 @@
case 'Enter':
if (event.ctrlKey || event.metaKey) {
event.preventDefault()
runPreview($previewArgs, undefined)
runPreview(previewArgs.val, undefined)
}
break
@@ -156,20 +156,20 @@
}
}
let savedArgs = $previewArgs
let savedArgs = previewArgs.val
let inputSelected: 'captures' | 'history' | 'saved' | undefined = undefined
async function selectInput(input, type?: 'captures' | 'history' | 'saved' | undefined) {
if (!input) {
$previewArgs = savedArgs
previewArgs.val = savedArgs
inputSelected = undefined
setTimeout(() => {
preventEscape = false
}, 100)
} else {
$previewArgs = input
previewArgs.val = input
inputSelected = type
preventEscape = true
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
}
}
@@ -185,7 +185,7 @@
async function loadIndividualStepsStates() {
// console.log('loadIndividualStepsStates')
dfs($flowStore.value.modules, async (module) => {
dfs(flowStore.val.value.modules, async (module) => {
// console.log('module', $flowStateStore[module.id], module.id)
const prev = $flowStateStore[module.id]?.previewResult
if (prev && prev != NEVER_TESTED_THIS_FAR) {
@@ -283,7 +283,7 @@
variant="border"
title={`Re-start this flow from step ${selectedJobStep} (included).`}
on:click={() => {
runPreview($previewArgs, {
runPreview(previewArgs.val, {
flow_job_id: jobId,
step_id: selectedJobStep,
branch_or_iteration_n: 0
@@ -308,7 +308,7 @@
color="blue"
startIcon={{ icon: RefreshCw }}
on:click={() => {
runPreview($previewArgs, {
runPreview(previewArgs.val, {
flow_job_id: jobId,
step_id: selectedJobStep,
branch_or_iteration_n: 0
@@ -354,7 +354,7 @@
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Restart flow"
on:click|once={() => {
runPreview($previewArgs, {
runPreview(previewArgs.val, {
flow_job_id: jobId,
step_id: selectedJobStep,
branch_or_iteration_n: branchOrIterationN
@@ -375,7 +375,7 @@
color="dark"
size="sm"
btnClasses="w-full max-w-lg"
on:click={() => runPreview($previewArgs, undefined)}
on:click={() => runPreview(previewArgs.val, undefined)}
id="flow-editor-test-flow-drawer"
shortCut={{ Icon: CornerDownLeft }}
>
@@ -385,7 +385,7 @@
{/if}
</div>
<div class="w-full flex flex-col gap-y-1">
{#if lastPreviewFlow && JSON.stringify($flowStore) != lastPreviewFlow}
{#if lastPreviewFlow && JSON.stringify(flowStore.val) != lastPreviewFlow}
<div class="pt-1">
<div
class="bg-orange-200 text-orange-600 border border-orange-600 p-2 flex items-center gap-2 rounded"
@@ -409,7 +409,7 @@
runnableId={$initialPathStore}
stablePathForCaptures={$initialPathStore || fakeInitialPath}
runnableType={'FlowPath'}
previewArgs={$previewArgs}
previewArgs={previewArgs.val}
on:openTriggers
on:select={(e) => {
selectInput(e.detail.payload, e.detail?.type)
@@ -433,7 +433,7 @@
}}
lightMode
on:change={(e) => {
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
refresh()
}}
/>
@@ -445,7 +445,7 @@
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
$previewArgs = e.detail
previewArgs.val = e.detail
}
}}
updateOnBlur={false}
@@ -458,10 +458,10 @@
<SchemaForm
noVariablePicker
compact
schema={$flowStore.schema}
bind:args={$previewArgs}
schema={flowStore.val.schema}
bind:args={previewArgs.val}
on:change={() => {
savedArgs = $previewArgs
savedArgs = previewArgs.val
}}
bind:isValid
/>
@@ -29,7 +29,7 @@
import Alert from './common/alert/Alert.svelte'
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
import { buildPrefix } from './graph/graphBuilder'
import { buildPrefix } from './graph/graphBuilder.svelte'
const dispatch = createEventDispatcher()
@@ -1253,31 +1253,29 @@
success={jobId != undefined && isSuccess(job?.['success'])}
flowModuleStates={$localModuleStates}
bind:expandedSubflows
on:select={(e) => {
onSelect={(e) => {
console.log('onSelect', e)
if (rightColumnSelect != 'node_definition') {
rightColumnSelect = 'node_status'
}
if (typeof e.detail == 'string') {
if (e.detail == 'Input') {
if (typeof e == 'string') {
if (e == 'Input') {
selectedNode = 'start'
stepDetail = undefined
} else if (e.detail == 'Result') {
} else if (e == 'Result') {
selectedNode = 'end'
stepDetail = 'end'
} else {
const mod = dfs(job?.raw_flow?.modules ?? [], (m) => m).find(
(m) => m?.id === e?.detail
)
const mod = dfs(job?.raw_flow?.modules ?? [], (m) => m).find((m) => m?.id === e)
stepDetail = mod
selectedNode = e?.detail
selectedNode = e
}
} else {
stepDetail = e.detail
selectedNode = e.detail.id
stepDetail = e
selectedNode = e.id
}
}}
on:selectedIteration={async (e) => {
let detail = e.detail
onSelectedIteration={async (detail) => {
if (detail.manuallySet) {
let rootJobId = detail.id
await tick()
+24 -14
View File
@@ -8,20 +8,30 @@
import FlowViewerInner from './FlowViewerInner.svelte'
import FlowInputViewer from './FlowInputViewer.svelte'
export let flow: {
summary: string
description?: string
value: FlowValue
schema?: any
interface Props {
flow: {
summary: string
description?: string
value: FlowValue
schema?: any
}
initialOpen?: number | undefined
noSide?: boolean
noGraph?: boolean
tab?: 'ui' | 'raw' | 'schema'
noSummary?: boolean
noGraphDownload?: boolean
}
export let initialOpen: number | undefined = undefined
export let noSide = false
export let noGraph: boolean = false
export let tab: 'ui' | 'raw' | 'schema' = noGraph ? 'schema' : 'ui'
export let noSummary = false
export let noGraphDownload = false
let {
flow,
initialOpen = undefined,
noSide = false,
noGraph = false,
tab = $bindable(noGraph ? 'schema' : 'ui'),
noSummary = false,
noGraphDownload = false
}: Props = $props()
let open: { [id: number]: boolean } = {}
if (initialOpen) {
@@ -38,7 +48,7 @@
<Tab value="raw">Raw</Tab>
<Tab value="schema">Input Schema</Tab>
<svelte:fragment slot="content">
{#snippet content()}
<TabContent value="ui">
<div class="flow-root w-full pb-4">
{#if !noSummary}
@@ -65,5 +75,5 @@
<div class="my-4"></div>
<SchemaViewer schema={flow.schema} />
</TabContent>
</svelte:fragment>
{/snippet}
</Tabs>
@@ -11,16 +11,20 @@
import HighlightTheme from './HighlightTheme.svelte'
import { filteredContentForExport } from './flows/utils'
export let flow: {
summary: string
description?: string
value: FlowValue
schema?: any
interface Props {
flow: {
summary: string
description?: string
value: FlowValue
schema?: any
}
}
$: flowFiltered = filteredContentForExport(flow)
let { flow }: Props = $props()
let rawType: 'json' | 'yaml' = 'yaml'
let flowFiltered = $derived(filteredContentForExport(flow))
let rawType: 'json' | 'yaml' = $state('yaml')
function trimStringToLines(inputString: string, maxLines: number = 100): string {
const lines = inputString?.split('\n') ?? []
@@ -29,7 +33,7 @@
return linesToKeep.join('\n')
}
let code: string = ''
let code: string = $state('')
function computeCode() {
const str =
@@ -44,11 +48,13 @@
code = str
}
let shouldDisplayLoadMore = false
let shouldDisplayLoadMore = $state(false)
$: flowFiltered && rawType && computeCode()
$effect(() => {
flowFiltered && rawType && computeCode()
})
let maxLines = 100
let maxLines = $state(100)
</script>
<HighlightTheme />
@@ -62,7 +68,7 @@
>
<Tab value="yaml">YAML</Tab>
<Tab value="json">JSON</Tab>
<svelte:fragment slot="content">
{#snippet content()}
<div class="relative pt-2">
<Button
on:click={() =>
@@ -105,7 +111,7 @@
</Button>
{/if}
</div>
</svelte:fragment>
{/snippet}
</Tabs>
</div>
+27 -28
View File
@@ -184,13 +184,11 @@
</Alert>
<div class="flex items-center gap-1">
<div>
<ToggleButtonGroup
bind:selected={ownerKind}
on:selected={() => (ownerItem = '')}
let:item
>
<ToggleButton value="user" label="User" {item} />
<ToggleButton value="group" label="Group" {item} />
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (ownerItem = '')}>
{#snippet children({ item })}
<ToggleButton value="user" label="User" {item} />
<ToggleButton value="group" label="Group" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -278,7 +276,6 @@
<ToggleButtonGroup
disabled={owner_name == 'u/' + $userStore?.username && !$userStore?.is_admin}
selected={role}
let:item
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
@@ -315,28 +312,30 @@
loadFolder()
}}
>
<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}
/>
{#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="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}
/>
<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}
+23 -22
View File
@@ -150,7 +150,6 @@
<div>
<ToggleButtonGroup
selected={role}
let:item
on:selected={async (e) => {
const role = e.detail
// const wasInGroup = (group?.members ?? []).includes(group)
@@ -211,31 +210,33 @@
loadGroup()
}}
>
<ToggleButton
value="member"
size="xs"
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
{item}
/>
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
{item}
/>
{#if role === 'manager'}
{#snippet children({ item })}
<ToggleButton
value="manager"
value="member"
size="xs"
label="Manager"
tooltip="A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member"
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
{item}
/>
{/if}
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
{item}
/>
{#if role === 'manager'}
<ToggleButton
value="manager"
size="xs"
label="Manager"
tooltip="A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member"
{item}
/>
{/if}
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
@@ -8,21 +8,34 @@
import HistoricList from './HistoricList.svelte'
import { Loader2 } from 'lucide-svelte'
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let loading: boolean = false
export let selected: string | undefined = undefined
export let showAuthor = false
export let placement: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end' = 'top-end'
export let limitPayloadSize = false
export let searchArgs: Record<string, any> | undefined = undefined
interface Props {
runnableId?: string | undefined
runnableType?: RunnableType | undefined
loading?: boolean
selected?: string | undefined
showAuthor?: boolean
placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end'
limitPayloadSize?: boolean
searchArgs?: Record<string, any> | undefined
}
let historicList: HistoricList | undefined = undefined
let {
runnableId = undefined,
runnableType = undefined,
loading = $bindable(false),
selected = $bindable(undefined),
showAuthor = false,
placement = 'top-end',
limitPayloadSize = false,
searchArgs = undefined
}: Props = $props()
let historicList: HistoricList | undefined = $state(undefined)
const dispatch = createEventDispatcher()
let jobs: Job[] = []
let jobs: Job[] = $state([])
let hasMoreCurrentRuns = false
let page = 1
let page = $state(1)
async function handleSelected(data: any) {
if (selected === data.id) {
@@ -34,7 +47,10 @@
const fullPayload = await data.getFullPayload?.()
dispatch('select', { args: fullPayload, jobId: data.id })
} else {
dispatch('select', { args: structuredClone(data.payloadData), jobId: data.id })
dispatch('select', {
args: structuredClone($state.snapshot(data.payloadData)),
jobId: data.id
})
}
}
@@ -77,11 +93,11 @@
} else if (runnableType === 'ScriptHash') {
return 'script,preview'
}
return 'all'
return ''
}
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window onkeydown={handleKeydown} />
{#if runnableId}
<JobLoader
+31 -17
View File
@@ -1,21 +1,33 @@
<script lang="ts">
import { InputService, type RunnableType } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { onDestroy } from 'svelte'
import { onDestroy, untrack } from 'svelte'
import InfiniteList from './InfiniteList.svelte'
import JobSchemaPicker from './schema/JobSchemaPicker.svelte'
export let runnableId: string | undefined
export let runnableType: RunnableType | undefined
export let selected: string | undefined = undefined
export let showAuthor = false
export let placement: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end' = 'bottom-start'
export let limitPayloadSize = false
export let searchArgs: Record<string, any> | undefined = undefined
interface Props {
runnableId: string | undefined
runnableType: RunnableType | undefined
selected?: string | undefined
showAuthor?: boolean
placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end'
limitPayloadSize?: boolean
searchArgs?: Record<string, any> | undefined
}
let infiniteList: InfiniteList | undefined = undefined
let {
runnableId,
runnableType,
selected = undefined,
showAuthor = false,
placement = 'bottom-start',
limitPayloadSize = false,
searchArgs = undefined
}: Props = $props()
let infiniteList: InfiniteList | undefined = $state(undefined)
let loadInputsPageFn: ((page: number, perPage: number) => Promise<any>) | undefined = undefined
let viewerOpen = false
let viewerOpen = $state(false)
let openStates: Record<string, boolean> = {} // Track open state for each item
export async function refresh(clearCurrentRuns: boolean = false) {
@@ -101,18 +113,20 @@
viewerOpen = Object.values(openStates).some((state) => state)
}
$: $workspaceStore && runnableId && runnableType && infiniteList && initLoadInputs()
$effect(() => {
$workspaceStore && runnableId && runnableType && infiniteList && untrack(() => initLoadInputs())
})
</script>
<InfiniteList bind:this={infiniteList} selectedItemId={selected} on:error on:select>
<svelte:fragment slot="columns">
{#snippet columns()}
<colgroup>
<col class="w-8" />
<col class="w-16" />
<col />
</colgroup>
</svelte:fragment>
<svelte:fragment let:item let:hover>
{/snippet}
{#snippet children({ item, hover })}
<JobSchemaPicker
job={item}
hovering={hover}
@@ -125,10 +139,10 @@
}}
{limitPayloadSize}
/>
</svelte:fragment>
<svelte:fragment slot="empty">
{/snippet}
{#snippet empty()}
<div class="text-center text-tertiary text-xs py-2">
{runnableId ? 'No previous inputs' : 'Save draft to see previous runs'}
</div>
</svelte:fragment>
{/snippet}
</InfiniteList>
@@ -1,18 +1,35 @@
<script>
import { FileText } from 'lucide-svelte'
import { APP_TO_ICON_COMPONENT } from './icons'
export let name
export let silent = false
export let after = false
export let height = '24px'
export let width = '24px'
export let center = false
export let isSelected = false
export let formatExtension = undefined
/**
* @typedef {Object} Props
* @property {any} name
* @property {boolean} [silent]
* @property {boolean} [after]
* @property {string} [height]
* @property {string} [width]
* @property {boolean} [center]
* @property {boolean} [isSelected]
* @property {any} [formatExtension]
*/
$: iconComponent = name === "teams"
? APP_TO_ICON_COMPONENT.ms_teams_webhook
: APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
/** @type {Props} */
let {
name,
silent = false,
after = false,
height = '24px',
width = '24px',
center = false,
isSelected = false,
formatExtension = undefined
} = $props()
let iconComponent = $derived(
name === 'teams'
? APP_TO_ICON_COMPONENT.ms_teams_webhook
: APP_TO_ICON_COMPONENT[name] || APP_TO_ICON_COMPONENT[name.split('_')[0]]
)
</script>
<div class="truncate flex flex-row gap-2 {center ? 'justify-center items-center' : ''} -pl-2">
@@ -20,12 +37,13 @@
{name}
{/if}
{#if iconComponent}
{@const SvelteComponent = iconComponent}
<span class={isSelected ? 'text-secondary' : 'text-secondary grayscale'}>
<svelte:component this={iconComponent} {height} {width} />
<SvelteComponent {height} {width} />
</span>
{:else if formatExtension}
<span class={isSelected ? 'text-secondary' : 'text-secondary grayscale'}>
<svelte:component this={FileText} {height} {width} />
<FileText {height} {width} />
</span>
{:else}
<span style="width: {width}; height: {height}" class="bg-gray-100 rounded-full"></span>
@@ -34,4 +52,3 @@
{name}
{/if}
</div>
+43 -21
View File
@@ -6,26 +6,48 @@
const dispatch = createEventDispatcher()
export let loading = false
export let items: any[] | undefined = undefined
export let selectedItemId: any | undefined = undefined
export let isEmpty: boolean = true
export let length: number = 0
export let rounded: boolean = true
export let noBorder: boolean = false
export let extraRowClasses: { bgSelected: string; bgHover: string; class: string } = {
bgSelected: '',
bgHover: '',
class: ''
interface Props {
loading?: boolean
items?: any[] | undefined
selectedItemId?: any | undefined
isEmpty?: boolean
length?: number
rounded?: boolean
noBorder?: boolean
extraRowClasses?: { bgSelected: string; bgHover: string; class: string }
neverShowLoader?: boolean
columns?: import('svelte').Snippet
extra_row?: import('svelte').Snippet<[any]>
children?: import('svelte').Snippet<[any]>
empty?: import('svelte').Snippet<[any]>
}
export let neverShowLoader = false
let {
loading = $bindable(false),
items = $bindable(undefined),
selectedItemId = $bindable(undefined),
isEmpty = $bindable(true),
length = $bindable(0),
rounded = true,
noBorder = false,
extraRowClasses = {
bgSelected: '',
bgHover: '',
class: ''
},
neverShowLoader = false,
columns,
extra_row,
children,
empty
}: Props = $props()
const perPage = 20
let hasMore = false
let hasMore = $state(false)
let page = 1
let hasAlreadyFailed = false
let hovered: any | undefined = undefined
let hovered: any | undefined = $state(undefined)
let initLoad = false
let loadInputs: ((page: number, perPage: number) => Promise<any[]>) | undefined = undefined
let deleteItemFn: ((id: any) => Promise<any>) | undefined = undefined
@@ -39,7 +61,7 @@
page = 1
}
let loadingMore = false
let loadingMore = $state(false)
export async function loadData(loadOption: 'refresh' | 'forceRefresh' | 'loadMore' = 'loadMore') {
// console.log('loadData', loadOption, length, items?.length)
@@ -146,7 +168,7 @@
{noBorder}
{neverShowLoader}
>
<slot name="columns" />
{@render columns?.()}
<tbody class="h-full w-full">
<Row
@@ -158,7 +180,7 @@
)}
on:hover={(e) => (hovered = e.detail ? 'extraRow' : undefined)}
>
<slot name="extra-row" hover={hovered === 'extraRow'} />
{@render extra_row?.({ hover: hovered === 'extraRow' })}
</Row>
{#each items ?? [] as item, index}
{@const hover = item.id === hovered}
@@ -171,16 +193,16 @@
)}
on:hover={(e) => (hovered = e.detail ? item.id : undefined)}
>
<slot {item} {hover} />
{@render children?.({ item, hover })}
</Row>
{/each}
</tbody>
<svelte:fragment slot="emptyMessage">
{#snippet emptyMessage()}
{#if (!items || items?.length === 0) && (!loading || neverShowLoader)}
<slot name="empty" {items} />
{@render empty?.({ items })}
{/if}
</svelte:fragment>
{/snippet}
</DataTable>
<style>
@@ -1,4 +1,4 @@
<script context="module">
<script module>
const dynamicTemplateRegexPairs = buildPrefixRegex([
'flow_input',
'results',
@@ -10,7 +10,7 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import type { InputCat } from '$lib/utils'
import { createEventDispatcher, getContext } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import ArgInput from './ArgInput.svelte'
import FieldHeader from './FieldHeader.svelte'
@@ -37,40 +37,61 @@
import { twMerge } from 'tailwind-merge'
import FlowPlugConnect from './FlowPlugConnect.svelte'
import { deepEqual } from 'fast-equals'
export let schema: Schema | { properties?: Record<string, any>; required?: string[] }
export let arg: InputTransform | any
export let argName: string
export let headerTooltip: string | undefined = undefined
export let headerTooltipIconClass = ''
export let HeaderTooltipIcon = InfoIcon
export let extraLib: string = 'missing extraLib'
export let inputCheck: boolean = true
export let previousModuleId: string | undefined
export let pickForField: string | undefined = undefined
export let variableEditor: VariableEditor | undefined = undefined
export let itemPicker: ItemPicker | undefined = undefined
export let noDynamicToggle = false
export let argExtra: Record<string, any> = {}
export let pickableProperties: PickableProperties | undefined = undefined
export let enableAi = false
export let hideHelpButton = false
let monaco: SimpleEditor | undefined = undefined
let monacoTemplate: TemplateEditor | undefined = undefined
let argInput: ArgInput | undefined = undefined
interface Props {
schema: Schema | { properties?: Record<string, any>; required?: string[] }
arg: InputTransform | any
argName: string
headerTooltip?: string | undefined
headerTooltipIconClass?: string
HeaderTooltipIcon?: any
extraLib?: string
inputCheck?: boolean
previousModuleId: string | undefined
pickForField?: string | undefined
variableEditor?: VariableEditor | undefined
itemPicker?: ItemPicker | undefined
noDynamicToggle?: boolean
argExtra?: Record<string, any>
pickableProperties?: PickableProperties | undefined
enableAi?: boolean
hideHelpButton?: boolean
class?: string
editor?: SimpleEditor | undefined
}
let {
schema = $bindable(),
arg = $bindable(),
argName = $bindable(),
headerTooltip = undefined,
headerTooltipIconClass = '',
HeaderTooltipIcon = InfoIcon,
extraLib = $bindable('missing extraLib'),
inputCheck = $bindable(true),
previousModuleId,
pickForField = $bindable(undefined),
variableEditor = undefined,
itemPicker = undefined,
noDynamicToggle = false,
argExtra = {},
pickableProperties = undefined,
enableAi = false,
hideHelpButton = false,
class: className = '',
editor = $bindable(undefined)
}: Props = $props()
let monaco: SimpleEditor | undefined = $state(undefined)
let monacoTemplate: TemplateEditor | undefined = $state(undefined)
let argInput: ArgInput | undefined = $state(undefined)
let focusedPrev = false
const dispatch = createEventDispatcher()
$: inputCat = computeInputCat(
schema?.properties?.[argName]?.type,
schema?.properties?.[argName]?.format,
schema?.properties?.[argName]?.items?.type,
schema?.properties?.[argName]?.enum,
schema?.properties?.[argName]?.contentEncoding
)
let propertyType = getPropertyType(arg)
$effect(() => {
editor = monaco
})
const { shouldUpdatePropertyType, exprsToSet } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
@@ -79,6 +100,18 @@
getContext<PropPickerWrapperContext>('PropPickerWrapper')
const { inputMatches, focusProp, propPickerConfig, clearFocus } = propPickerWrapperContext ?? {}
let inputCat = $derived(
computeInputCat(
schema?.properties?.[argName]?.type,
schema?.properties?.[argName]?.format,
schema?.properties?.[argName]?.items?.type,
schema?.properties?.[argName]?.enum,
schema?.properties?.[argName]?.contentEncoding
)
)
let propertyType = $state(getPropertyType(arg))
function setExpr() {
const newArg = $exprsToSet?.[argName]
if (newArg) {
@@ -98,8 +131,6 @@
})
}
$: $exprsToSet?.[argName] && setExpr()
function updatePropertyType() {
propertyType = $shouldUpdatePropertyType?.[argName] || 'static'
shouldUpdatePropertyType?.set({
@@ -108,10 +139,6 @@
})
}
$: $shouldUpdatePropertyType?.[argName] &&
arg?.type === $shouldUpdatePropertyType?.[argName] &&
updatePropertyType()
function getPropertyType(arg: InputTransform | any): 'static' | 'javascript' {
let type: 'static' | 'javascript' = arg?.type ?? 'static'
@@ -153,7 +180,7 @@
}
}
let codeInjectionDetected = false
let codeInjectionDetected = $state(false)
function checkCodeInjection(rawValue: string) {
if (!arg || !rawValue || rawValue.length < 3 || !dynamicTemplateRegexPairs) {
@@ -247,11 +274,10 @@
}
let prevArg: any = undefined
$: inputCat && propertyType && arg && onArgChange()
function onArgChange() {
const newArg = { arg, propertyType, inputCat }
if (!deepEqual(newArg, prevArg)) {
prevArg = structuredClone(newArg)
prevArg = structuredClone($state.snapshot(newArg))
updateStaticInput(inputCat, propertyType, arg)
}
}
@@ -285,23 +311,39 @@
}
focusedPrev = focused
}
$: updateFocused(focused)
$: schema?.properties?.[argName]?.default && setDefaultCode()
let resourceTypes: string[] | undefined = undefined
let resourceTypes: string[] | undefined = $state(undefined)
async function loadResourceTypes() {
resourceTypes = await getResourceTypes()
}
let focused = false
let stepInputGen: StepInputGen | undefined = undefined
let focused = $state(false)
let stepInputGen: StepInputGen | undefined = $state(undefined)
loadResourceTypes()
$: connecting =
$effect(() => {
$exprsToSet?.[argName] && untrack(() => setExpr())
})
$effect(() => {
$shouldUpdatePropertyType?.[argName] &&
arg?.type === $shouldUpdatePropertyType?.[argName] &&
untrack(() => updatePropertyType())
})
$effect(() => {
inputCat && propertyType && arg && untrack(() => onArgChange())
})
$effect(() => {
;[focused]
untrack(() => updateFocused(focused))
})
$effect(() => {
schema?.properties?.[argName]?.default && untrack(() => setDefaultCode())
})
let connecting = $derived(
$propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'
)
</script>
{#if arg != undefined}
@@ -311,7 +353,7 @@
$propPickerConfig?.propName == argName
? 'bg-surface border-l-4 border-blue-500 shadow-md rounded-l-md z-50 '
: 'hover:rounded-md',
$$props.class
className
)}
>
<div class="flex flex-row justify-between gap-1 pb-1 px-2">
@@ -375,7 +417,6 @@
<div>
<ToggleButtonGroup
selected={propertyType}
let:item
on:selected={(e) => {
if (e.detail == propertyType) return
const staticTemplate = isStaticTemplate(inputCat)
@@ -428,41 +469,43 @@
}
}}
>
{#if isStaticTemplate(inputCat)}
<ToggleButton
tooltip={`Write text or surround javascript with \`\$\{\` and \`\}\`. Use \`results\` to connect to another node\'s output.`}
light
value="static"
size="xs2"
label={'${}'}
{item}
/>
{:else}
<ToggleButton small label="Static" value="static" {item} />
{/if}
{#snippet children({ item })}
{#if isStaticTemplate(inputCat)}
<ToggleButton
tooltip={`Write text or surround javascript with \`\$\{\` and \`\}\`. Use \`results\` to connect to another node\'s output.`}
light
value="static"
size="xs2"
label={'${}'}
{item}
/>
{:else}
<ToggleButton small label="Static" value="static" {item} />
{/if}
{#if codeInjectionDetected && propertyType == 'static'}
<Button
size="xs2"
color="light"
btnClasses="font-normal text-xs w-fit bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-300 dark:bg-green-700 dark:hover:bg-green-600"
on:click={() => setJavaScriptExpr(arg.value)}
>
<span class="font-normal whitespace-nowrap flex gap-2 items-center"
><FunctionSquare size={14} /> detected -
<span class="font-bold">TAB</span>
</span>
</Button>
{:else}
<ToggleButton
small
light
tooltip="JavaScript expression ('flow_input' or 'results')."
value="javascript"
icon={FunctionSquare}
{item}
/>
{/if}
{#if codeInjectionDetected && propertyType == 'static'}
<Button
size="xs2"
color="light"
btnClasses="font-normal text-xs w-fit bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-300 dark:bg-green-700 dark:hover:bg-green-600"
on:click={() => setJavaScriptExpr(arg.value)}
>
<span class="font-normal whitespace-nowrap flex gap-2 items-center"
><FunctionSquare size={14} /> detected -
<span class="font-bold">TAB</span>
</span>
</Button>
{:else}
<ToggleButton
small
light
tooltip="JavaScript expression ('flow_input' or 'results')."
value="javascript"
icon={FunctionSquare}
{item}
/>
{/if}
{/snippet}
</ToggleButtonGroup>
</div>
@@ -491,8 +534,8 @@
</div>
<div class="max-w-xs"></div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="relative" on:keyup={handleKeyUp}>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="relative" onkeyup={handleKeyUp}>
<!-- {#if $propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'}
<span
class={'text-white z-50 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute top-0 right-0 bg-blue-500'}
@@ -3,7 +3,7 @@
import { VariableService, type InputTransform } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { Button } from './common'
import StepInputsGen from './copilot/StepInputsGen.svelte'
import type { PickableProperties } from './flows/previousResults'
@@ -12,30 +12,45 @@
import VariableEditor from './VariableEditor.svelte'
import { Plus } from 'lucide-svelte'
export let schema: Schema | { properties?: Record<string, any> }
export let args: Record<string, InputTransform | any> = {}
interface Props {
schema: Schema | { properties?: Record<string, any> }
args?: Record<string, InputTransform | any>
isValid?: boolean
extraLib?: string
previousModuleId?: string | undefined
filter?: string[] | undefined
noDynamicToggle?: boolean
pickableProperties?: PickableProperties | undefined
enableAi?: boolean
class?: string
}
export let isValid: boolean = true
export let extraLib: string = 'missing extraLib'
export let previousModuleId: string | undefined = undefined
let {
schema = $bindable(),
args = $bindable({}),
isValid = $bindable(true),
extraLib = $bindable('missing extraLib'),
previousModuleId = undefined,
filter = undefined,
noDynamicToggle = false,
pickableProperties = undefined,
enableAi = false,
class: clazz = ''
}: Props = $props()
export let filter: string[] | undefined = undefined
export let noDynamicToggle = false
export let pickableProperties: PickableProperties | undefined = undefined
export let enableAi = false
let clazz: string = ''
export { clazz as class }
let inputCheck: { [id: string]: boolean } = {}
let inputCheck: { [id: string]: boolean } = $state({})
const dispatch = createEventDispatcher()
$: isValid = allTrue(inputCheck) ?? false
$effect(() => {
isValid = allTrue(inputCheck) ?? false
})
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
$effect(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
export function setArgs(nargs: Record<string, InputTransform | any>) {
args = nargs
@@ -51,18 +66,18 @@
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let pickForField: string | undefined = $state()
let itemPicker: ItemPicker | undefined = $state(undefined)
let variableEditor: VariableEditor | undefined = $state(undefined)
let keys: string[] = []
$: {
let keys: string[] = $state([])
$effect(() => {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
removeExtraKey()
untrack(() => removeExtraKey())
}
}
})
</script>
<div class="w-full {clazz}">
@@ -84,15 +99,17 @@
</div>
{/if}
{#if keys.length > 0}
{#each keys as argName (argName)}
{#each keys as argName, index (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
<div class="pt-2 relative">
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
bind:schema
bind:argName
bind:inputCheck={inputCheck[argName]}
bind:argName={keys[index]}
bind:inputCheck={
() => inputCheck[argName] ?? false, (value) => (inputCheck[argName] = value)
}
bind:extraLib
{variableEditor}
{itemPicker}
@@ -128,22 +145,21 @@
...x
}))}
>
<div
slot="submission"
class="flex flex-row-reverse w-full border-t border-gray-200 rounded-bl-lg rounded-br-lg"
>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew?.()
}}
>
New variable
</Button>
</div>
{#snippet submission()}
<div class="flex flex-row-reverse w-full border-t border-gray-200 rounded-bl-lg rounded-br-lg">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew?.()
}}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
@@ -218,15 +218,17 @@
</span>
{/if}
</label>
<ToggleButtonGroup bind:selected={$values[setting.key]} let:item={toggleButton}>
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButton}
/>
{/each}
<ToggleButtonGroup bind:selected={$values[setting.key]}>
{#snippet children({ item: toggleButton })}
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButton}
/>
{/each}
{/snippet}
</ToggleButtonGroup>
</div>
{:else if setting.fieldType == 'select_python'}
@@ -241,52 +243,55 @@
{/if}
</label>
<ToggleButtonGroup bind:selected={$values[setting.key]} let:item={toggleButtonn}>
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButtonn}
/>
{/each}
<PopoverMelt closeButton={!isPyFetching}>
<svelte:fragment slot="trigger">
{#if setting.select_items?.some((e) => e.label == $values[setting.key] || e.value == $values[setting.key])}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs bg-surface-secondary border-0"
nonCaptureEvent={true}>Select Custom</Button
>
{:else}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs border-0 shadow-md"
nonCaptureEvent={true}>Custom | {$values[setting.key]}</Button
>
{/if}
</svelte:fragment>
<svelte:fragment slot="content">
{#if isPyFetching}
<div class="p-4">
<LoadingIcon />
</div>
{:else}
<ToggleButtonGroup
bind:selected={$values[setting.key]}
let:item={toggleButtonn}
class="mr-10 h-full"
tabListClass="flex-wrap p-2"
>
{#each pythonAvailableVersions as item}
<ToggleButton value={item} label={item} tooltip={item} item={toggleButtonn} />
{/each}
</ToggleButtonGroup>
{/if}
</svelte:fragment>
</PopoverMelt>
<ToggleButtonGroup bind:selected={$values[setting.key]}>
{#snippet children({ item: toggleButtonn })}
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButtonn}
/>
{/each}
<PopoverMelt closeButton={!isPyFetching}>
<svelte:fragment slot="trigger">
{#if setting.select_items?.some((e) => e.label == $values[setting.key] || e.value == $values[setting.key])}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs bg-surface-secondary border-0"
nonCaptureEvent={true}>Select Custom</Button
>
{:else}
<Button
variant="border"
color="dark"
btnClasses="px-1.5 py-1.5 text-2xs border-0 shadow-md"
nonCaptureEvent={true}>Custom | {$values[setting.key]}</Button
>
{/if}
</svelte:fragment>
<svelte:fragment slot="content">
{#if isPyFetching}
<div class="p-4">
<LoadingIcon />
</div>
{:else}
<ToggleButtonGroup
bind:selected={$values[setting.key]}
class="mr-10 h-full"
tabListClass="flex-wrap p-2"
>
{#snippet children({ item: toggleButtonn })}
{#each pythonAvailableVersions as item}
<ToggleButton value={item} label={item} tooltip={item} item={toggleButtonn} />
{/each}
{/snippet}
</ToggleButtonGroup>
{/if}
</svelte:fragment>
</PopoverMelt>
{/snippet}
</ToggleButtonGroup>
</div>
{:else}
@@ -246,7 +246,7 @@
<Tab value={category}>{category}</Tab>
{/each}
<svelte:fragment slot="content">
{#snippet content()}
{#each Object.keys(settings) as category}
<TabContent value={category}>
{#if category == 'SMTP'}
@@ -362,7 +362,7 @@
</div>
</TabContent>
{/each}
</svelte:fragment>
{/snippet}
</Tabs>
</div>
+29 -27
View File
@@ -13,7 +13,7 @@
const dispatch = createEventDispatcher()
let email: string
let email: string = $state('')
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
@@ -52,7 +52,7 @@
goto('#superadmin-settings')
}
}
]
]
: []
)
}
@@ -62,45 +62,47 @@
email = ''
}
let selected: 'operator' | 'developer' | 'admin' = 'developer'
let selected: 'operator' | 'developer' | 'admin' = $state('developer')
</script>
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: MailPlus }}>
Invite
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<div class="flex flex-col gap-2 p-4">
<input
type="email"
on:keyup={handleKeyUp}
onkeyup={handleKeyUp}
placeholder="email"
bind:value={email}
class="mr-4"
/>
<ToggleButtonGroup bind:selected let:item>
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButtonGroup bind:selected>
{#snippet children({ item })}
<ToggleButton
value="operator"
label="Operator"
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="developer"
label="Developer"
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
<ToggleButton
value="admin"
label="Admin"
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
{item}
/>
{/snippet}
</ToggleButtonGroup>
<Button
variant="contained"
@@ -112,5 +114,5 @@
Invite
</Button>
</div>
</svelte:fragment>
{/snippet}
</Popover>
+43 -25
View File
@@ -7,23 +7,41 @@
import SearchItems from './SearchItems.svelte'
type Item = Record<string, any>
export let pickCallback: (path: string, f: string) => void
export let loadItems: () => Promise<Item[] | undefined>
export let extraField: string = 'path'
export let extraField2: string | undefined = undefined
export let itemName: string
export let closeOnClick = true
/** Displayed if the load function returns no items. */
export let noItemMessage = 'There are no items in the list'
/** Displayed if the search returns no items. */
export let buttons: Record<string, (x: string) => void> = {}
export let tooltip: string = ''
export let documentationLink: string | undefined = undefined
let loading = false
let items: Item[] | undefined = []
let filteredItems: Item[] | undefined = []
let filter = ''
interface Props {
pickCallback: (path: string, f: string) => void
loadItems: () => Promise<Item[] | undefined>
extraField?: string
extraField2?: string | undefined
itemName: string
closeOnClick?: boolean
/** Displayed if the load function returns no items. */
noItemMessage?: string
/** Displayed if the search returns no items. */
buttons?: Record<string, (x: string) => void>
tooltip?: string
documentationLink?: string | undefined
submission?: import('svelte').Snippet
}
let {
pickCallback,
loadItems,
extraField = 'path',
extraField2 = undefined,
itemName,
closeOnClick = true,
noItemMessage = 'There are no items in the list',
buttons = {},
tooltip = '',
documentationLink = undefined,
submission
}: Props = $props()
let loading = $state(false)
let items: Item[] | undefined = $state([])
let filteredItems: Item[] | undefined = $state([])
let filter = $state('')
export function openDrawer() {
loading = true
@@ -34,12 +52,12 @@
.finally(() => {
loading = false
})
drawer.openDrawer?.()
drawer?.openDrawer?.()
}
let drawer: Drawer
let drawer: Drawer | undefined = $state()
let refreshing = false
let refreshing = $state(false)
</script>
<SearchItems
@@ -65,7 +83,7 @@
>
<div class="w-full h-full flex flex-col">
<div class="flex flex-row gap-2 pb-4">
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
placeholder="Search {itemName}s"
@@ -110,9 +128,9 @@
<button
class="py-2 px-1 gap-1 flex grow border-gray-300 border-opacity-0
text-primary"
on:click={() => {
onclick={() => {
if (closeOnClick) {
drawer.closeDrawer()
drawer?.closeDrawer()
}
pickCallback(obj['path'], obj[extraField])
}}
@@ -174,8 +192,8 @@
<NoItemFound />
{/if}
</div>
<svelte:fragment slot="actions">
<slot name="submission" />
</svelte:fragment>
{#snippet actions()}
{@render submission?.()}
{/snippet}
</DrawerContent>
</Drawer>
+2 -2
View File
@@ -109,7 +109,7 @@ ${Object.entries(args)
<Drawer bind:this={jsonViewer} size="900px">
<DrawerContent title="Expanded Args" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
download="windmill-args.json"
href={id && workspace
@@ -137,7 +137,7 @@ ${Object.entries(args)
>
Copy to clipboard
</Button>
</svelte:fragment>
{/snippet}
{#if jsonStr.length > 100000 || (id && workspace && args && isWindmillTooBigObject(args))}
<div class="text-sm mb-2 text-tertiary">
<a
+2 -2
View File
@@ -149,7 +149,7 @@
<Drawer bind:this={logViewer} bind:open={drawerOpen} size="900px">
<DrawerContent title="Expanded Logs" on:close={logViewer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
{#if jobId && download}
<Button
href="{base}/api/w/{$workspaceStore}/jobs_u/get_logs/{jobId}"
@@ -174,7 +174,7 @@
>
Copy to clipboard
</Button>
</svelte:fragment>
{/snippet}
<div>
<pre
class="bg-surface-secondary text-secondary text-xs w-full p-2 whitespace-pre-wrap border rounded-md"
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import type { Schema } from '$lib/common'
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -16,31 +18,47 @@
import { evalValue } from './flows/utils'
import type { PickableProperties } from './flows/previousResults'
export let mod: FlowModule
export let schema: Schema | { properties?: Record<string, any> }
export let pickableProperties: PickableProperties | undefined
export let testJob: Job | undefined = undefined
export let testIsLoading = false
export let noEditor = false
export let scriptProgress = undefined
interface Props {
mod: FlowModule
schema: Schema | { properties?: Record<string, any> }
pickableProperties: PickableProperties | undefined
testJob?: Job | undefined
testIsLoading?: boolean
noEditor?: boolean
scriptProgress?: any
}
let {
mod,
schema,
pickableProperties,
testJob = $bindable(undefined),
testIsLoading = $bindable(false),
noEditor = false,
scriptProgress = $bindable(undefined)
}: Props = $props()
const { flowStore, flowStateStore, testStepStore, pathStore } =
getContext<FlowEditorContext>('FlowEditorContext')
// Test
let testJobLoader: TestJobLoader
let testJobLoader: TestJobLoader | undefined = $state()
let jobProgressReset: () => void = () => {}
let stepArgs: Record<string, any> | undefined = Object.fromEntries(
Object.keys(schema.properties ?? {}).map((k) => [
k,
evalValue(k, mod, $testStepStore, pickableProperties, false)
])
let stepArgs: Record<string, any> | undefined = $state(
Object.fromEntries(
Object.keys(schema.properties ?? {}).map((k) => [
k,
evalValue(k, mod, $testStepStore, pickableProperties, false)
])
)
)
$: $testStepStore[mod.id] = stepArgs
run(() => {
$testStepStore[mod.id] = stepArgs
})
export function runTestWithStepArgs() {
runTest(stepArgs)
@@ -58,7 +76,7 @@
val.content,
val.language,
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
$flowStore?.tag ?? val.tag
flowStore.val?.tag ?? val.tag
)
} else if (val.type == 'script') {
const script = val.hash
@@ -69,7 +87,7 @@
script.content,
script.language,
mod.id === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
$flowStore?.tag ?? (val.tag_override ? val.tag_override : script.tag),
flowStore.val?.tag ?? (val.tag_override ? val.tag_override : script.tag),
script.lock,
val.hash ?? script.hash
)
@@ -104,7 +122,7 @@
/>
<div class="p-4">
{#if $flowStore.value.same_worker}
{#if flowStore.val.value.same_worker}
<div class="mb-1 bg-yellow-100 text-yellow-700 p-1 text-xs"
>The `./shared` folder is not passed across individual "Test this step"</div
>
@@ -5,7 +5,7 @@
import { RefreshCw } from 'lucide-svelte'
import ArgInput from './ArgInput.svelte'
import { Button } from './common'
import { getContext } from 'svelte'
import { getContext, untrack } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import { evalValue } from './flows/utils'
import type { FlowModule } from '$lib/gen'
@@ -13,22 +13,36 @@
import type SimpleEditor from './SimpleEditor.svelte'
import { getResourceTypes } from './resourceTypesStore'
export let schema: Schema | { properties?: Record<string, any>; required?: string[] }
export let args: Record<string, any> = {}
export let mod: FlowModule
export let pickableProperties: PickableProperties | undefined
interface Props {
schema: Schema | { properties?: Record<string, any>; required?: string[] }
args?: Record<string, any>
mod: FlowModule
pickableProperties: PickableProperties | undefined
isValid?: boolean
autofocus?: boolean
}
export let isValid: boolean = true
export let autofocus = false
let {
schema,
args = $bindable({}),
mod,
pickableProperties,
isValid = $bindable(true),
autofocus = false
}: Props = $props()
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
let inputCheck: { [id: string]: boolean } = {}
$: isValid = allTrue(inputCheck) ?? false
let inputCheck: { [id: string]: boolean } = $state({})
$effect(() => {
isValid = allTrue(inputCheck) ?? false
})
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
$effect(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
function removeExtraKey() {
const nargs = {}
@@ -40,14 +54,14 @@
args = nargs
}
let keys: string[] = []
$: {
let keys: string[] = $state([])
$effect(() => {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
removeExtraKey()
untrack(() => removeExtraKey())
}
}
})
function plugIt(argName: string) {
args[argName] = structuredClone(
@@ -60,9 +74,9 @@
}
}
let editor: Record<string, SimpleEditor | undefined> = {}
let editor: Record<string, SimpleEditor | undefined> = $state({})
let resourceTypes: string[] | undefined = undefined
let resourceTypes: string[] | undefined = $state(undefined)
async function loadResourceTypes() {
resourceTypes = await getResourceTypes()
@@ -11,32 +11,54 @@
import { getContext } from 'svelte'
import { getStringError } from './copilot/chat/utils'
export let lang: Script['language']
export let editor: Editor | undefined
export let diffEditor: DiffEditor | undefined
export let loopStatus:
| { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' }
| undefined = undefined
export let lastJob: Job | undefined = undefined
export let scriptProgress: number | undefined = undefined
export let testJob: Job | undefined = undefined
export let mod: FlowModule
export let testIsLoading: boolean = false
export let disableMock: boolean = false
export let disableHistory: boolean = false
interface Props {
lang: Script['language']
editor: Editor | undefined
diffEditor: DiffEditor | undefined
loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
lastJob?: Job | undefined
scriptProgress?: number | undefined
testJob?: Job | undefined
mod: FlowModule
testIsLoading?: boolean
disableMock?: boolean
disableHistory?: boolean
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
}
const { flowStore, testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
let {
lang,
editor,
diffEditor,
loopStatus = undefined,
lastJob = undefined,
scriptProgress = $bindable(undefined),
testJob = undefined,
mod,
testIsLoading = false,
disableMock = false,
disableHistory = false,
onUpdateMock
}: Props = $props()
let selectedJob: Job | undefined = undefined
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
let selectedJob: Job | undefined = $state(undefined)
let fetchingLastJob = false
let preview: 'mock' | 'job' | undefined = undefined
let outputPicker: OutputPickerInner | undefined = undefined
let jobProgressReset: () => void = () => {}
let preview: 'mock' | 'job' | undefined = $state(undefined)
let jobProgressReset: () => void = $state(() => {})
$: lastJob && outputPicker?.setLastJob(lastJob, false)
$: testJob && outputPicker?.setLastJob(testJob, true)
let nlastJob = $derived.by(() => {
if (testJob) {
return { ...testJob, preview: true }
}
if (lastJob) {
return { ...lastJob, preview: false }
}
return undefined
})
let forceJson = false
let forceJson = $state(false)
</script>
<Splitpanes horizontal>
@@ -51,15 +73,12 @@
{/if}
<OutputPickerInner
bind:this={outputPicker}
lastJob={nlastJob}
fullResult
moduleId={mod.id}
closeOnOutsideClick={true}
getLogs
on:updateMock={({ detail }) => {
mod.mock = detail
$flowStore = $flowStore
}}
{onUpdateMock}
mock={mod.mock}
bind:forceJson
bind:selectedJob
@@ -70,11 +89,11 @@
{disableMock}
{disableHistory}
>
<svelte:fragment slot="copilot-fix">
{#snippet copilot_fix()}
{#if lang && editor && diffEditor && $testStepStore[mod.id] && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)}
<ScriptFix {lang} />
{/if}
</svelte:fragment>
{/snippet}
</OutputPickerInner>
</Pane>
<Pane size={50} minSize={10}>
@@ -117,8 +117,8 @@
<div class="flex flex-col mb-2 gap-6">
<Path disabled={!own} {kind} {initialPath} bind:path />
</div>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button disabled={!own} on:click={updatePath}>Move/Rename</Button>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+15 -8
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import CollapseLink from './CollapseLink.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
@@ -6,11 +8,11 @@
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
export let value: any
interface Props {
value: any
}
$: enabled = value != undefined
$: changeDomain(value?.['domain'], value?.['custom'])
let { value = $bindable() }: Props = $props()
function changeDomain(domain, custom) {
if (value) {
@@ -31,10 +33,14 @@
}
}
}
let enabled = $derived(value != undefined)
run(() => {
changeDomain(value?.['domain'], value?.['custom'])
})
</script>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-sm font-medium text-primary flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name="okta" after={true} /></div><Toggle
checked={enabled}
@@ -57,10 +63,11 @@
on:selected={({ detail }) => {
value['custom'] = detail === 'custom'
}}
let:item
>
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{#snippet children({ item })}
<ToggleButton value="org" label={'Org'} {item} />
<ToggleButton value="custom" label="Custom" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
<div class="grow">
+105 -82
View File
@@ -1,4 +1,4 @@
<script lang="ts" context="module">
<script lang="ts" module>
const lastMetaUsed = writable<Meta | undefined>(undefined)
</script>
@@ -24,7 +24,7 @@
GcpTriggerService
} from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher, getContext } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import { writable } from 'svelte/store'
import { Alert, Button } from './common'
import Badge from './common/badge/Badge.svelte'
@@ -52,27 +52,42 @@
| 'mqtt_trigger'
| 'sqs_trigger'
| 'gcp_trigger'
let meta: Meta | undefined = undefined
export let fullNamePlaceholder: string | undefined = undefined
export let namePlaceholder = ''
export let initialPath: string
export let path = ''
export let error = ''
export let disabled = false
export let checkInitialPathExistence = false
export let autofocus = true
export let dirty = false
export let kind: PathKind
export let hideUser: boolean = false
export let disableEditing = false
let meta: Meta | undefined = $state(undefined)
interface Props {
fullNamePlaceholder?: string | undefined
namePlaceholder?: string
initialPath: string
path?: string
error?: string
disabled?: boolean
checkInitialPathExistence?: boolean
autofocus?: boolean
dirty?: boolean
kind: PathKind
hideUser?: boolean
disableEditing?: boolean
}
let inputP: HTMLInputElement | undefined = undefined
let {
fullNamePlaceholder = undefined,
namePlaceholder = '',
initialPath,
path = $bindable(''),
error = $bindable(''),
disabled = $bindable(false),
checkInitialPathExistence = false,
autofocus = true,
dirty = $bindable(false),
kind,
hideUser = false,
disableEditing = false
}: Props = $props()
let inputP: HTMLInputElement | undefined = $state(undefined)
const dispatch = createEventDispatcher()
let folders: { name: string; write: boolean }[] = []
$: meta && onMetaChange()
let folders: { name: string; write: boolean }[] = $state([])
function onMetaChange() {
if (meta) {
@@ -204,6 +219,7 @@
}
async function pathExists(path: string, kind: PathKind): Promise<boolean> {
if (!path.length) return false
if (kind == 'flow') {
return await FlowService.existsFlowByPath({ workspace: $workspaceStore!, path: path })
} else if (kind == 'script') {
@@ -288,13 +304,6 @@
}
}
$: {
if ($workspaceStore && $userStore) {
loadFolders()
initPath()
}
}
async function initPath() {
await tick()
if (path != undefined && path != '') {
@@ -319,45 +328,58 @@
'openSearchWithPrefilledText'
)
$: displayPathChangedWarning =
$effect(() => {
;[meta?.name, meta?.owner, meta?.ownerKind]
meta && untrack(() => onMetaChange())
})
$effect(() => {
if ($workspaceStore && $userStore) {
untrack(() => {
loadFolders()
initPath()
})
}
})
let displayPathChangedWarning = $derived(
(['flow', 'script', 'resource', 'variable'] as PathKind[]).includes(kind) &&
initialPath &&
initialPath !== path
$: pathUsageInFlowsPromise =
initialPath &&
initialPath !== path
)
let pathUsageInFlowsPromise = $derived(
(kind == 'script' || kind == 'flow') &&
$workspaceStore &&
initialPath &&
FlowService.listFlowPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
$: pathUsageInAppsPromise =
$workspaceStore &&
initialPath &&
FlowService.listFlowPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
)
let pathUsageInAppsPromise = $derived(
(kind == 'script' || kind == 'flow') &&
$workspaceStore &&
initialPath &&
AppService.listAppPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
$: pathUsageInScriptsPromise =
$workspaceStore &&
initialPath &&
AppService.listAppPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath,
runnableKind: kind
})
)
let pathUsageInScriptsPromise = $derived(
kind == 'script' &&
$workspaceStore &&
initialPath &&
ScriptService.listScriptPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath
})
$workspaceStore &&
initialPath &&
ScriptService.listScriptPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
path: initialPath
})
)
</script>
<div>
<div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 pb-0 mb-1">
{#if meta != undefined}
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
{#if !hideUser}
<div class="block">
<ToggleButtonGroup
@@ -377,29 +399,30 @@
}
}}
disabled={disabled || disableEditing}
let:item
>
<ToggleButton
icon={User}
disabled={disabled || disableEditing}
light
size="xs"
value="user"
position="left"
label="User"
{item}
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton
icon={Folder}
disabled={disabled || disableEditing}
light
size="xs"
value="folder"
position="right"
label="Folder"
{item}
/>
{#snippet children({ item })}
<ToggleButton
icon={User}
disabled={disabled || disableEditing}
light
size="xs"
value="user"
position="left"
label="User"
{item}
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton
icon={Folder}
disabled={disabled || disableEditing}
light
size="xs"
value="folder"
position="right"
label="Folder"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>
{/if}
@@ -417,7 +440,7 @@
disabled={disabled ||
!($superadmin || ($userStore?.is_admin ?? false)) ||
disableEditing}
on:keydown={setDirty}
onkeydown={setDirty}
/>
</label>
{:else if meta.ownerKind === 'folder'}
@@ -428,7 +451,7 @@
</div>
<span class="text-xl">/</span>
<label class="block grow w-full max-w-md">
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
disabled={disabled || disableEditing}
type="text"
@@ -436,7 +459,7 @@
{autofocus}
bind:this={inputP}
autocomplete="off"
on:keyup={handleKeyUp}
onkeyup={handleKeyUp}
bind:value={meta.name}
placeholder={namePlaceholder}
class={error === ''
@@ -461,7 +484,7 @@
value={path}
size={path?.length || 50}
class="font-mono !text-xs max-w-[calc(100%-70px)] !w-auto !h-[24px] !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
onfocus={({ currentTarget }) => {
currentTarget.select()
}}
/>
@@ -59,9 +59,12 @@
queuedJobs = loadingQueuedJobs
const endStart = new Date().getTime()
// toggle queuedJobsLoading to false in 1 secs to let some time for the animation to play
setTimeout(() => {
queuedJobsLoading = false
}, 3000 - (endStart - timeStart))
setTimeout(
() => {
queuedJobsLoading = false
},
3000 - (endStart - timeStart)
)
}
async function scaleToZero() {
@@ -167,18 +170,20 @@
</tbody>
</TableCustom>
<div slot="actions" class="flex gap-1">
<Button
color="red"
disabled={cancellingInProgress === true || queuedJobs.length === 0}
on:click={scaleToZero}
>
{#if cancellingInProgress}
<Loader2 class="animate-spin" /> Stopping jobs
{:else}
Scale down to 0
{/if}
</Button>
</div>
{#snippet actions()}
<div class="flex gap-1">
<Button
color="red"
disabled={cancellingInProgress === true || queuedJobs.length === 0}
on:click={scaleToZero}
>
{#if cancellingInProgress}
<Loader2 class="animate-spin" /> Stopping jobs
{:else}
Scale down to 0
{/if}
</Button>
</div>
{/snippet}
</DrawerContent>
</Drawer>
+1 -1
View File
@@ -57,6 +57,6 @@
export let name = undefined
</script>
<div use:portal={{ target, name }} hidden>
<div use:portal={{ target, name }} hidden class={$$props.class}>
<slot />
</div>
@@ -1,8 +1,10 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import type { Schema } from '$lib/common'
import { ResourceService, type Resource, type ResourceType } from '$lib/gen'
import { canWrite, emptyString, isOwner, urlize } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { Alert, Skeleton } from './common'
import Path from './Path.svelte'
import Required from './Required.svelte'
@@ -20,44 +22,48 @@
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
export let canSave = true
export let resource_type: string | undefined = undefined
export let path: string = ''
export let newResource: boolean = false
export let hidePath: boolean = false
export let watchChanges: boolean = false
export let defaultValues: Record<string, any> | undefined = undefined
$: if (defaultValues && Object.keys(defaultValues).length > 0) {
args = defaultValues
interface Props {
canSave?: boolean
resource_type?: string | undefined
path?: string
newResource?: boolean
hidePath?: boolean
watchChanges?: boolean
defaultValues?: Record<string, any> | undefined
}
let isValid = true
let jsonError = ''
let can_write = true
let {
canSave = $bindable(true),
resource_type = $bindable(undefined),
path = $bindable(''),
newResource = false,
hidePath = false,
watchChanges = false,
defaultValues = undefined
}: Props = $props()
$: canSave = can_write && isValid && jsonError == ''
let isValid = $state(true)
let jsonError = $state('')
let can_write = $state(true)
let initialPath = path
let resourceToEdit: Resource | undefined = undefined
let resourceToEdit: Resource | undefined = $state(undefined)
let description: string = ''
let description: string = $state('')
let DESCRIPTION_PLACEHOLDER = `Describe what this resource is for`
let resourceSchema: Schema | undefined = undefined
let args: Record<string, any> = {}
let loadingSchema = true
let linkedVars: string[] = []
let resourceTypeInfo: ResourceType | undefined = undefined
let editDescription = false
let viewJsonSchema = false
let resourceSchema: Schema | undefined = $state(undefined)
let args: Record<string, any> = $state({})
let loadingSchema = $state(true)
let linkedVars: string[] = $state([])
let resourceTypeInfo: ResourceType | undefined = $state(undefined)
let editDescription = $state(false)
let viewJsonSchema = $state(false)
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
$: watchChanges && dispatchIfMounted('change', { path, args, description })
let rawCode: string | undefined = undefined
let rawCode: string | undefined = $state(undefined)
async function initEdit() {
resourceToEdit = await ResourceService.getResource({ workspace: $workspaceStore!, path })
@@ -81,9 +87,6 @@
sendUserToast('Resource type cannot be undefined for new resource creation', true)
}
$: rawCode && parseJson()
$: linkedVars.length > 0 && path && updateArgsFromLinkedVars()
export async function editResource(): Promise<void> {
if (resourceToEdit) {
await ResourceService.updateResource({
@@ -150,8 +153,7 @@
})
}
let textFileContent: string = ''
$: textFileContent && parseTextFileContent()
let textFileContent: string = $state('')
function switchTab(asJson: boolean) {
viewJsonSchema = asJson
@@ -170,6 +172,26 @@
content: textFileContent
}
}
run(() => {
if (defaultValues && Object.keys(defaultValues).length > 0) {
args = defaultValues
}
})
run(() => {
canSave = can_write && isValid && jsonError == ''
})
run(() => {
watchChanges && dispatchIfMounted('change', { path, args, description })
})
run(() => {
rawCode && untrack(() => parseJson())
})
run(() => {
linkedVars.length > 0 && path && untrack(() => updateArgsFromLinkedVars())
})
run(() => {
textFileContent && untrack(() => parseTextFileContent())
})
</script>
<div>
@@ -57,7 +57,7 @@
bind:canSave
/>
{/await}
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
startIcon={{ icon: Save }}
on:click={() => {
@@ -72,6 +72,6 @@
>
Save
</Button>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+82 -52
View File
@@ -20,49 +20,13 @@
import JsonInputs from '$lib/components/JsonInputs.svelte'
import TriggerableByAI from './TriggerableByAI.svelte'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import { untrack } from 'svelte'
export let runnable:
| {
summary?: string
schema?: Schema | any
description?: string
path?: string
is_template?: boolean
hash?: string
kind?: string
can_write?: boolean
created_at?: string
created_by?: string
extra_perms?: Record<string, boolean>
}
| undefined
export let runAction: (
scheduledForStr: string | undefined,
args: Record<string, any>,
invisible_to_owner: boolean | undefined,
overrideTag: string | undefined
) => void
export let buttonText = 'Run'
export let schedulable = true
export let detailed = true
export let autofocus = false
export let topButton = false
export let loading = false
export let noVariablePicker = false
export let viewKeybinding = false
export let scheduledForStr: string | undefined
export let invisible_to_owner: boolean | undefined
export let overrideTag: string | undefined
export let args: Record<string, any> = {}
export let jsonView = false
let reloadArgs = 0
let jsonEditor: JsonInputs | undefined = undefined
let schemaHeight = 0
let showInputSelectedBadge = false
let savedPreviousArgs: Record<string, any> | undefined = undefined
let reloadArgs = $state(0)
let jsonEditor: JsonInputs | undefined = $state(undefined)
let schemaHeight = $state(0)
let showInputSelectedBadge = $state(false)
let savedPreviousArgs: Record<string, any> | undefined = $state(undefined)
export async function setArgs(nargs: Record<string, any>) {
args = nargs
@@ -70,12 +34,72 @@
}
export function run() {
runAction(scheduledForStr, args, invisible_to_owner, overrideTag)
runAction(scheduledForStr, args ?? {}, invisible_to_owner, overrideTag)
}
export let isValid = true
interface Props {
runnable:
| {
summary?: string
schema?: Schema | any
description?: string
path?: string
is_template?: boolean
hash?: string
kind?: string
can_write?: boolean
created_at?: string
created_by?: string
extra_perms?: Record<string, boolean>
}
| undefined
runAction: (
scheduledForStr: string | undefined,
args: Record<string, any>,
invisible_to_owner: boolean | undefined,
overrideTag: string | undefined
) => void
buttonText?: string
schedulable?: boolean
detailed?: boolean
autofocus?: boolean
topButton?: boolean
loading?: boolean
noVariablePicker?: boolean
viewKeybinding?: boolean
scheduledForStr: string | undefined
invisible_to_owner: boolean | undefined
overrideTag: string | undefined
args?: Record<string, any>
jsonView?: boolean
isValid?: boolean
}
let {
runnable = $bindable(),
runAction,
buttonText = 'Run',
schedulable = true,
detailed = true,
autofocus = false,
topButton = false,
loading = false,
noVariablePicker = false,
viewKeybinding = false,
scheduledForStr = $bindable(),
invisible_to_owner = $bindable(),
overrideTag = $bindable(),
args = $bindable(),
jsonView = false,
isValid = $bindable(true)
}: Props = $props()
$effect.pre(() => {
if (args == undefined) {
args = {}
}
})
$: onArgsChange(args)
let debounced: NodeJS.Timeout | undefined = undefined
function onArgsChange(args: any) {
@@ -99,6 +123,12 @@
export function setCode(code: string) {
jsonEditor?.setCode(code)
}
$effect(() => {
Object.keys(args ?? {}).forEach((key) => {
args?.[key]
})
untrack(() => onArgsChange(args))
})
</script>
<TriggerableByAI
@@ -195,7 +225,7 @@
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
on:click={() => runAction(undefined, args ?? {}, invisible_to_owner, overrideTag)}
>
{buttonText}
</Button>
@@ -249,26 +279,26 @@
color="dark"
btnClasses="!px-6 !py-1 !h-8 inline-flex gap-2"
disabled={!isValid || jsonView}
on:click={() => runAction(scheduledForStr, args, invisible_to_owner, overrideTag)}
on:click={() => runAction(scheduledForStr, args ?? {}, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
{scheduledForStr ? 'Schedule to run later' : buttonText}
</Button>
<div>
<Popover placement="bottom" closeButton usePointerDownOutside>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button nonCaptureEvent startIcon={{ icon: Calendar }} size="xs" color="light">
Advanced
</Button>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<RunFormAdvancedPopup
bind:scheduledForStr
bind:invisible_to_owner
bind:overrideTag
bind:runnable
/>
</svelte:fragment>
{/snippet}
</Popover>
</div>
</div>
@@ -287,7 +317,7 @@
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
on:click={() => runAction(undefined, args ?? {}, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
{buttonText}
+164 -140
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { run, createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import {
File as FileIcon,
FolderClosed,
@@ -17,47 +20,57 @@
import { Alert, Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Section from './Section.svelte'
import { createEventDispatcher } from 'svelte'
import VirtualList from 'svelte-tiny-virtual-list'
import { createEventDispatcher, untrack } from 'svelte'
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
import TableSimple from './TableSimple.svelte'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import FileUploadModal from './common/fileUpload/FileUploadModal.svelte'
import PdfViewer from './display/PdfViewer.svelte'
import { twMerge } from 'tailwind-merge'
let deletionModalOpen = false
let fileDeletionInProgress = false
let deletionModalOpen = $state(false)
let fileDeletionInProgress = $state(false)
let fileListUnavailable: boolean | undefined = undefined
let fileListUnavailable: boolean | undefined = $state(undefined)
let moveModalOpen = false
let moveDestKey: string | undefined = undefined
let fileMoveInProgress = false
let moveModalOpen = $state(false)
let moveDestKey: string | undefined = $state(undefined)
let fileMoveInProgress = $state(false)
let uploadModalOpen = false
let uploadModalOpen = $state(false)
let workspaceSettingsInitialized = true
let workspaceSettingsInitialized = $state(true)
export let fromWorkspaceSettings: boolean = false
export let readOnlyMode: boolean
export let initialFileKey: { s3: string; storage?: string } | undefined = undefined
let initialFileKeyInternalCopy: { s3: string; storage?: string }
export let selectedFileKey: { s3: string; storage?: string } | undefined = undefined
export let folderOnly = false
export let regexFilter: RegExp | undefined = undefined
interface Props {
fromWorkspaceSettings?: boolean
readOnlyMode: boolean
initialFileKey?: { s3: string; storage?: string } | undefined
selectedFileKey?: { s3: string; storage?: string } | undefined
folderOnly?: boolean
regexFilter?: RegExp | undefined
}
let csvSeparatorChar: string = ','
let csvHasHeader: boolean = true
let {
fromWorkspaceSettings = false,
readOnlyMode,
initialFileKey = $bindable(undefined),
selectedFileKey = $bindable(undefined),
folderOnly = false,
regexFilter = undefined
}: Props = $props()
let csvSeparatorChar: string = $state(',')
let csvHasHeader: boolean = $state(true)
let dispatch = createEventDispatcher<{
close: { s3: string; storage: string | undefined } | undefined
}>()
let drawer: Drawer
let drawer: Drawer | undefined = $state()
let fileInfoLoading: boolean = true
let fileListLoading: boolean = true
let fileInfoLoading: boolean = $state(true)
let fileListLoading: boolean = $state(true)
let allFilesByKey: Record<
string,
{
@@ -69,10 +82,10 @@
nestingLevel: number
count: number
}
> = {}
let displayedFileKeys: string[] = []
> = $state({})
let displayedFileKeys: string[] = $state([])
let listDivHeight: number = 0
let listDivHeight: number = $state(0)
let fileMetadata:
| {
@@ -82,29 +95,28 @@
sizeStr: string | undefined
lastModified: string | undefined
}
| undefined = undefined
let filePreviewLoading: boolean = false
| undefined = $state(undefined)
let filePreviewLoading: boolean = $state(false)
let filePreview:
| {
fileKey: string
contentPreview: string | undefined
contentType: string | undefined
}
| undefined = undefined
| undefined = $state(undefined)
let listMarkers: string[]
let page = 0
let page = $state(0)
const maxKeys = 1000
let count = 0
let displayedCount = 0
let count = $state(0)
let displayedCount = $state(0)
let filter = ''
let filter = $state('')
let timeout: NodeJS.Timeout | undefined = undefined
let firstLoad = true
$: filter != undefined && onFilterChange()
function onFilterChange() {
if (!firstLoad) {
@@ -117,7 +129,7 @@
}
}
let lastKeyFolders: string[] = []
let lastKeyFolders: string[] = $state([])
async function loadFiles() {
fileListLoading = true
let availableFiles = await HelpersService.listStoredFiles({
@@ -364,7 +376,7 @@
await loadFileMetadataPlusPreviewAsync(selectedFileKey.s3)
}
let storage: string | undefined = undefined
let storage: string | undefined = $state(undefined)
export async function open(
preSelectedFileKey: { s3: string; storage?: string } | undefined = undefined
) {
@@ -374,7 +386,7 @@
selectedFileKey = { ...preSelectedFileKey }
}
reloadContent()
drawer.openDrawer?.()
drawer?.openDrawer?.()
}
async function reloadContent() {
@@ -405,14 +417,14 @@
}
async function selectAndClose() {
drawer.closeDrawer?.()
drawer?.closeDrawer?.()
}
async function exit() {
if (initialFileKeyInternalCopy !== undefined) {
selectedFileKey = { ...initialFileKeyInternalCopy }
}
drawer.closeDrawer?.()
drawer?.closeDrawer?.()
}
function selectItem(index: number, toggleCollapsed: boolean = true) {
@@ -463,10 +475,13 @@
loadFileMetadataPlusPreviewAsync(selectedFileKey.s3)
}
}
run(() => {
filter != undefined && untrack(() => onFilterChange())
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<Drawer
bind:this={drawer}
on:close={() => {
@@ -551,51 +566,55 @@
itemCount={displayedFileKeys.length}
itemSize={42}
>
<div
slot="item"
let:index
let:style
{style}
class={twMerge(
'hover:bg-surface-hover border-b',
index === displayedFileKeys.length - 1 && 'border-b-0'
)}
>
{#snippet header()}{/snippet}
{#snippet footer()}{/snippet}
{#snippet children({ index, style })}
{@const file_info = allFilesByKey[displayedFileKeys[index]]}
{#if file_info}
<div
on:click={() => selectItem(index)}
class={twMerge(
'flex flex-row h-full font-semibold text-xs items-center justify-start',
selectedFileKey !== undefined && selectedFileKey.s3 === file_info.full_key
? 'bg-surface-hover'
: ''
)}
>
<div
{style}
class={twMerge(
'hover:bg-surface-hover border-b',
index === displayedFileKeys.length - 1 && 'border-b-0'
)}
>
{#if file_info}
<div
class={`flex flex-row w-full gap-2 h-full items-center`}
style={`margin-left: ${(2 + file_info.nestingLevel) * 0.25}rem;`}
onclick={() => selectItem(index)}
class={twMerge(
'flex flex-row h-full font-semibold text-xs items-center justify-start',
selectedFileKey !== undefined &&
selectedFileKey.s3 === file_info.full_key
? 'bg-surface-hover'
: ''
)}
>
{#if file_info.type === 'folder'}
{#if file_info.collapsed}<FolderClosed size={16} />{:else}<FolderOpen
size={16}
/>{/if}
<div class="truncate text-ellipsis w-56">
{file_info.display_name} ({file_info.count}{count % 1000 === 0 &&
lastKeyFolders[file_info.nestingLevel / 2] === file_info.display_name
? '+'
: ''} item{file_info.count === 1 ? '' : 's'})
</div>
{:else}
<FileIcon size={16} />
<div class="truncate text-ellipsis w-56">
{file_info.display_name}
</div>
{/if}
<div
class={`flex flex-row w-full gap-2 h-full items-center`}
style={`margin-left: ${(2 + file_info.nestingLevel) * 0.25}rem;`}
>
{#if file_info.type === 'folder'}
{#if file_info.collapsed}<FolderClosed size={16} />{:else}<FolderOpen
size={16}
/>{/if}
<div class="truncate text-ellipsis w-56">
{file_info.display_name} ({file_info.count}{count % 1000 === 0 &&
lastKeyFolders[file_info.nestingLevel / 2] ===
file_info.display_name
? '+'
: ''} item{file_info.count === 1 ? '' : 's'})
</div>
{:else}
<FileIcon size={16} />
<div class="truncate text-ellipsis w-56">
{file_info.display_name}
</div>
{/if}
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/snippet}
</VirtualList>
</div>
<div
@@ -643,42 +662,44 @@
{:else}
<div class="p-4 gap-2">
<Section label={fileMetadata.fileKey} breakAll>
<div slot="action" class="flex gap-2">
{#if filePreview !== undefined}
<Button
title="Download file from S3"
variant="border"
color="light"
href={`${base}/api/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(
fileMetadata?.fileKey ?? ''
)}${storage ? `&storage=${storage}` : ''}`}
download={fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
startIcon={{ icon: Download }}
iconOnly={true}
/>
<Button
title="Move file"
variant="border"
color="light"
on:click={() => {
moveDestKey = fileMetadata?.fileKey ?? ''
moveModalOpen = true
}}
startIcon={{ icon: MoveRight }}
iconOnly={true}
/>
<Button
title="Delete file from S3"
variant="border"
color="red"
on:click={() => {
deletionModalOpen = true
}}
startIcon={{ icon: Trash }}
iconOnly={true}
/>
{/if}
</div>
{#snippet action()}
<div class="flex gap-2">
{#if filePreview !== undefined}
<Button
title="Download file from S3"
variant="border"
color="light"
href={`${base}/api/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(
fileMetadata?.fileKey ?? ''
)}${storage ? `&storage=${storage}` : ''}`}
download={fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
startIcon={{ icon: Download }}
iconOnly={true}
/>
<Button
title="Move file"
variant="border"
color="light"
on:click={() => {
moveDestKey = fileMetadata?.fileKey ?? ''
moveModalOpen = true
}}
startIcon={{ icon: MoveRight }}
iconOnly={true}
/>
<Button
title="Delete file from S3"
variant="border"
color="red"
on:click={() => {
deletionModalOpen = true
}}
startIcon={{ icon: Trash }}
iconOnly={true}
/>
{/if}
</div>
{/snippet}
</Section>
<TableSimple
headers={['Last modified', 'Size', 'Type']}
@@ -721,7 +742,7 @@
<select
class="h-8"
bind:value={csvSeparatorChar}
on:change={(e) =>
onchange={(e) =>
loadFilePreview(
fileMetadata?.fileKey ?? '',
fileMetadata?.size,
@@ -737,19 +758,20 @@
Header row:
<div class="inline-flex item-center w-4 ml-2 mr-2">
<input
on:focus
on:click
onfocus={bubble('focus')}
onclick={bubble('click')}
disabled={false}
type="checkbox"
id="csv-header"
class="h-5"
bind:checked={csvHasHeader}
on:change|stopPropagation={(e) =>
onchange={stopPropagation((e) =>
loadFilePreview(
fileMetadata?.fileKey ?? '',
fileMetadata?.size,
fileMetadata?.mimeType
)}
)
)}
/>
</div>
{:else}
@@ -766,27 +788,29 @@
</div>
{/if}
<div slot="actions" class="flex gap-1">
{#if !readOnlyMode}
<Button
variant="border"
color="light"
disabled={workspaceSettingsInitialized === false}
startIcon={{ icon: FileUp }}
on:click={() => {
uploadModalOpen = true
}}>Upload File</Button
>
{#if !fromWorkspaceSettings}
{#snippet actions()}
<div class="flex gap-1">
{#if !readOnlyMode}
<Button
disabled={selectedFileKey === undefined ||
emptyString(selectedFileKey.s3) ||
(folderOnly && allFilesByKey[selectedFileKey.s3]?.type !== 'folder')}
on:click={selectAndClose}>Select</Button
variant="border"
color="light"
disabled={workspaceSettingsInitialized === false}
startIcon={{ icon: FileUp }}
on:click={() => {
uploadModalOpen = true
}}>Upload File</Button
>
{#if !fromWorkspaceSettings}
<Button
disabled={selectedFileKey === undefined ||
emptyString(selectedFileKey.s3) ||
(folderOnly && allFilesByKey[selectedFileKey.s3]?.type !== 'folder')}
on:click={selectAndClose}>Select</Button
>
{/if}
{/if}
{/if}
</div>
</div>
{/snippet}
</DrawerContent>
</Drawer>
@@ -3,7 +3,7 @@
import { InputService, type Input, type RunnableType } from '$lib/gen/index.js'
import { userStore, workspaceStore } from '$lib/stores.js'
import { sendUserToast } from '$lib/utils.js'
import { createEventDispatcher, onDestroy } from 'svelte'
import { createEventDispatcher, onDestroy, untrack } from 'svelte'
import { Edit, Trash2, Save } from 'lucide-svelte'
import Toggle from './Toggle.svelte'
import { Cell } from './table/index'
@@ -13,13 +13,25 @@
import { twMerge } from 'tailwind-merge'
import SavedInputsPickerViewer from './SavedInputsPickerViewer.svelte'
export let previewArgs: any = undefined
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let isValid: boolean = false
export let noButton: boolean = false
export let jsonView: boolean = false
export let limitPayloadSize: boolean = false
interface Props {
previewArgs?: any
runnableId?: string | undefined
runnableType?: RunnableType | undefined
isValid?: boolean
noButton?: boolean
jsonView?: boolean
limitPayloadSize?: boolean
}
let {
previewArgs = undefined,
runnableId = undefined,
runnableType = undefined,
isValid = false,
noButton = false,
jsonView = false,
limitPayloadSize = false
}: Props = $props()
interface EditableInput extends Input {
isEditing?: boolean
@@ -30,13 +42,13 @@
getFullPayload?: () => Promise<any>
}
let infiniteList: InfiniteList | null = null
let draft = true
let selectedInput: string | null = null
let isEditing: EditableInput | null = null
let viewerOpen = false
let openStates: Record<string, boolean> = {}
let clientWidth: number = 0
let infiniteList: InfiniteList | null = $state(null)
let draft = $state(true)
let selectedInput: string | null = $state(null)
let isEditing: EditableInput | null = $state(null)
let viewerOpen = $state(false)
let openStates: Record<string, boolean> = $state({})
let clientWidth: number = $state(0)
const dispatch = createEventDispatcher()
@@ -125,7 +137,7 @@
const fullPayload = await input.getFullPayload?.()
dispatch('select', fullPayload)
} else {
selectedArgs = structuredClone(input.payloadData ?? {})
selectedArgs = structuredClone($state.snapshot(input.payloadData) ?? {})
dispatch('select', selectedArgs)
}
}
@@ -185,25 +197,27 @@
viewerOpen = Object.values(openStates).some((state) => state)
}
$: $workspaceStore &&
runnableId &&
runnableType &&
(infiniteList && initLoadInputs(), (draft = false))
$effect(() => {
$workspaceStore &&
runnableId &&
runnableType &&
(infiniteList && untrack(() => initLoadInputs()), (draft = false))
})
</script>
<svelte:window on:keydown={handleKeydown} />
<svelte:window onkeydown={handleKeydown} />
<div class="w-full flex flex-col gap-1 h-full overflow-y-auto">
{#if !noButton}
<div>
<Popover class="w-full" placement="bottom" disablePopup={runnableId && previewArgs}>
<svelte:fragment slot="text">
{#snippet text()}
{#if !runnableId}
Save draft first before you can save inputs
{:else if !previewArgs}
Add inputs before saving
{/if}
</svelte:fragment>
{/snippet}
<SaveInputsButton
{runnableId}
{runnableType}
@@ -225,13 +239,13 @@
on:error={(e) => handleError(e.detail)}
on:select={(e) => handleSelect(e.detail)}
>
<svelte:fragment slot="columns">
{#snippet columns()}
<colgroup>
<col class="w-8" />
<col />
</colgroup>
</svelte:fragment>
<svelte:fragment let:item let:hover>
{/snippet}
{#snippet children({ item, hover })}
{@const editOptions =
item.created_by == $userStore?.username ||
$userStore?.is_admin ||
@@ -249,7 +263,7 @@
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
{#if isEditing && isEditing.id === item.id}
<form
on:submit={() => {
onsubmit={() => {
updateInput(isEditing)
setEditing(null)
}}
@@ -336,10 +350,10 @@
</div>
</div>
</Cell>
</svelte:fragment>
<svelte:fragment slot="empty">
{/snippet}
{#snippet empty()}
<div class="text-center text-xs text-tertiary">No saved Inputs</div>
</svelte:fragment>
{/snippet}
</InfiniteList>
{/if}
</div>
@@ -13,15 +13,28 @@
import MultiSelect from './multiselect/MultiSelectWrapper.svelte'
const dispatch = createEventDispatcher()
export let scriptHash: string | null = null
export let scriptPath: string | null = null
export let flowPath: string | null = null
export let inputSelected: 'saved' | 'history' | undefined = undefined
export let jsonView: boolean = false
export let schema: any
// Are the current Inputs valid and able to be saved?
export let isValid: boolean
export let args: object
interface Props {
scriptHash?: string | null
scriptPath?: string | null
flowPath?: string | null
inputSelected?: 'saved' | 'history' | undefined
jsonView?: boolean
schema: any
// Are the current Inputs valid and able to be saved?
isValid: boolean
args: object
}
let {
scriptHash = null,
scriptPath = null,
flowPath = null,
inputSelected = $bindable(undefined),
jsonView = false,
schema,
isValid,
args
}: Props = $props()
export function resetSelected() {
historicInputs?.resetSelected(true)
@@ -29,18 +42,14 @@
}
let savedArgs: any = undefined
let runnableType: RunnableType | undefined = undefined
let savedInputsPicker: SavedInputsPicker | undefined = undefined
let historicInputs: HistoricInputs | undefined = undefined
let savedInputsPicker: SavedInputsPicker | undefined = $state(undefined)
let historicInputs: HistoricInputs | undefined = $state(undefined)
$: runnableId = scriptHash || scriptPath || flowPath || undefined
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
let runnableId = $derived(scriptHash || scriptPath || flowPath || undefined)
let runnableType: RunnableType | undefined = $derived(
scriptHash ? 'ScriptHash' : scriptPath ? 'ScriptPath' : flowPath ? 'FlowPath' : undefined
)
function selectArgs(selected_args: any, type: 'saved' | 'history' | undefined) {
if (selected_args) {
@@ -55,18 +64,18 @@
}
}
let searchArgs = {}
let appliedSearchArgs = {}
let searchArgs = $state({})
let appliedSearchArgs = $state({})
let searchArgsFields = [] as string[]
let searchArgsFields = $state([] as string[])
$: emptySearchArgs = Object.keys(appliedSearchArgs).length === 0
let emptySearchArgs = $derived(Object.keys(appliedSearchArgs).length === 0)
$: filteredSchema = {
let filteredSchema = $derived({
properties: Object.fromEntries(
Object.entries(schema?.properties ?? {}).filter(([key]) => searchArgsFields.includes(key))
)
}
})
</script>
<div class="min-w-[300px] h-full flex flex-col">
@@ -78,7 +87,7 @@
wrapperClass="h-full"
small={true}
>
<svelte:fragment slot="action">
{#snippet action()}
<SaveInputsButton
{args}
disabled={!isValid || jsonView}
@@ -88,7 +97,7 @@
savedInputsPicker?.refresh()
}}
/>
</svelte:fragment>
{/snippet}
<SavedInputsPicker
bind:this={savedInputsPicker}
@@ -107,7 +116,7 @@
<Pane class="px-4 py-4 h-full">
<Section label="History" wrapperClass="h-full" small={true}>
<svelte:fragment slot="action">
{#snippet action()}
<div class="flex space-x-2">
<Popover
class="w-fit"
@@ -118,7 +127,7 @@
}}
floatingConfig={{ placement: 'top-end' }}
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button
variant="contained"
size="xs2"
@@ -130,9 +139,9 @@
>
{emptySearchArgs ? '' : 'Active filters'}
</Button>
</svelte:fragment>
{/snippet}
<svelte:fragment slot="content">
{#snippet content()}
<div id="multi-select-search"></div>
<div class="p-2 overflow-auto max-h-[400px] min-h-[300px] w-[400px]">
<div class="flex items-center flex-wrap gap-x-2 justify-between">
@@ -180,7 +189,7 @@
<div class="text-secondary text-sm my-8 mx-auto">No filters</div>
{/if}
</div>
</svelte:fragment>
{/snippet}
</Popover>
<Button
size="xs2"
@@ -194,7 +203,7 @@
All runs
</Button>
</div>
</svelte:fragment>
{/snippet}
<HistoricInputs
bind:this={historicInputs}
{runnableId}
+194 -123
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import type { Schema } from '$lib/common'
import { VariableService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -11,103 +14,145 @@
import { getResourceTypes } from './resourceTypesStore'
import { Plus } from 'lucide-svelte'
import ArgInput from './ArgInput.svelte'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { deepEqual } from 'fast-equals'
import { dragHandleZone, type Options as DndOptions } from '@windmill-labs/svelte-dnd-action'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { ComponentCustomCSS } from './apps/types'
export let schema: Schema | any
export let hiddenArgs: string[] = []
export let schemaFieldTooltip: Record<string, string> = {}
export let args: Record<string, any> = {}
export let disabledArgs: string[] = []
export let disabled = false
interface Props {
schema: Schema | any
hiddenArgs?: string[]
schemaFieldTooltip?: Record<string, string>
args?: Record<string, any>
disabledArgs?: string[]
disabled?: boolean
isValid?: boolean
autofocus?: boolean
defaultValues?: Record<string, any>
shouldHideNoInputs?: boolean
compact?: boolean
linkedSecret?: string | undefined
linkedSecretCandidates?: string[] | undefined
noVariablePicker?: boolean
flexWrap?: boolean
noDelete?: boolean
prettifyHeader?: boolean
disablePortal?: boolean
showSchemaExplorer?: boolean
showReset?: boolean
onlyMaskPassword?: boolean
dndConfig?: DndOptions | undefined
items?: { id: string; value: string }[] | undefined
helperScript?:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined
lightHeader?: boolean
diff?: Record<string, SchemaDiff>
nestedParent?: { label: string; nestedParent: any | undefined } | undefined
shouldDispatchChanges?: boolean
nestedClasses?: string
dynamicEnums?: Record<string, any>
largeGap?: boolean
css?: ComponentCustomCSS<'schemaformcomponent'> | undefined
displayType?: boolean
appPath?: string | undefined
className?: string
computeS3ForceViewerPolicies?:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined
workspace?: string | undefined
actions?: import('svelte').Snippet
}
export let isValid: boolean = true
export let autofocus = false
export let defaultValues: Record<string, any> = {}
let {
schema = $bindable(),
hiddenArgs = [],
schemaFieldTooltip = {},
args = $bindable(undefined),
disabledArgs = [],
disabled = false,
isValid = $bindable(true),
autofocus = false,
defaultValues = {},
shouldHideNoInputs = false,
compact = false,
linkedSecret = $bindable(undefined),
linkedSecretCandidates = undefined,
noVariablePicker = false,
flexWrap = false,
noDelete = false,
prettifyHeader = false,
disablePortal = false,
showSchemaExplorer = false,
showReset = false,
onlyMaskPassword = false,
dndConfig = undefined,
items = undefined,
helperScript = undefined,
lightHeader = false,
diff = {},
nestedParent = undefined,
shouldDispatchChanges = false,
nestedClasses = '',
dynamicEnums = {},
largeGap = false,
css = undefined,
displayType = true,
appPath = undefined,
className = '',
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
actions
}: Props = $props()
export let shouldHideNoInputs: boolean = false
export let compact = false
export let linkedSecret: string | undefined = undefined
export let linkedSecretCandidates: string[] | undefined = undefined
export let noVariablePicker = false
export let flexWrap = false
export let noDelete = false
export let prettifyHeader = false
export let disablePortal = false
export let showSchemaExplorer = false
export let showReset = false
export let onlyMaskPassword = false
export let dndConfig: DndOptions | undefined = undefined
export let items: { id: string; value: string }[] | undefined = undefined
export let helperScript:
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
| { type: 'hash'; hash: string }
| undefined = undefined
export let lightHeader = false
export let diff: Record<string, SchemaDiff> = {}
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
export let shouldDispatchChanges = false
export let nestedClasses = ''
export let dynamicEnums: Record<string, any> = {}
export let largeGap: boolean = false
export let css: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
export let displayType: boolean = true
export let appPath: string | undefined = undefined
export let computeS3ForceViewerPolicies:
| (() =>
| {
allowed_resources: string[]
allow_user_resources: boolean
allow_workspace_resource: boolean
file_key_regex: string
}
| undefined)
| undefined = undefined
export let workspace: string | undefined = undefined
$effect.pre(() => {
if (args == undefined) {
args = {}
}
})
const dispatch = createEventDispatcher()
let inputCheck: { [id: string]: boolean } = {}
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
let inputCheck: { [id: string]: boolean } = $state({})
export function setDefaults() {
const nargs = structuredClone(defaultValues)
const nargs = structuredClone($state.snapshot(defaultValues))
Object.keys(schema?.properties ?? {}).forEach((key) => {
if (schema?.properties[key].default != undefined && args[key] == undefined) {
if (schema?.properties[key].default != undefined && args && args[key] == undefined) {
let value = schema?.properties[key].default
nargs[key] = value === 'object' ? structuredClone(value) : value
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
}
})
args = nargs
}
let keys: string[]
$: keys = Array.isArray(schema?.order) ? schema?.order : Object.keys(schema?.properties ?? {})
let keys: string[] = $state([])
function removeExtraKey() {
const nargs = {}
Object.keys(args ?? {}).forEach((key) => {
if (keys.includes(key)) {
if (keys.includes(key) && args) {
nargs[key] = args[key]
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let pickForField: string | undefined = $state()
let itemPicker: ItemPicker | undefined = $state(undefined)
let variableEditor: VariableEditor | undefined = $state(undefined)
let resourceTypes: string[] | undefined = undefined
let resourceTypes: string[] | undefined = $state(undefined)
async function loadResourceTypes() {
resourceTypes = await getResourceTypes()
@@ -115,8 +160,6 @@
loadResourceTypes()
$: schema && (reorder(), (hidden = {}))
function hasExtraKeys() {
return Object.keys(args ?? {}).some((x) => !keys.includes(x))
}
@@ -147,20 +190,26 @@
dispatch('change')
}
}
// let missingKeys = keys.filter((x) => args && !(x in args))
// console.log('missingKeys', missingKeys)
// missingKeys.forEach((x) => {
// if (args) {
// args[x] = undefined
// }
// })
if (!noDelete && hasExtraKeys()) {
removeExtraKey()
}
}
$: fields = items ?? keys.map((x) => ({ id: x, value: x }))
let hidden: Record<string, boolean> = {}
let hidden: Record<string, boolean> = $state({})
let fields = $derived(items ?? keys.map((x) => ({ id: x, value: x })))
function handleHiddenFields(schema: Schema | any, args: Record<string, any>) {
for (const x of fields) {
if (schema?.properties[x.value]?.showExpr) {
if (computeShow(x.value, schema.properties[x.value].showExpr, args)) {
if (schema?.properties?.[x.value]?.showExpr) {
if (computeShow(x.value, schema.properties?.[x.value]?.showExpr, args)) {
hidden[x.value] = false
} else if (!hidden[x.value]) {
hidden[x.value] = true
@@ -173,12 +222,32 @@
}
}
$: handleHiddenFields(schema, args)
$: isValid = allTrue(inputCheck ?? {})
$effect(() => {
if (args == undefined || typeof args !== 'object') {
args = {}
}
})
$effect(() => {
const newKeys = Array.isArray(schema?.order)
? schema?.order
: Object.keys(schema?.properties ?? {})
if (!deepEqual(keys, newKeys)) {
keys = newKeys
}
})
$effect(() => {
schema && (untrack(() => reorder()), (hidden = {}))
})
$effect(() => {
;[schema, args]
untrack(() => handleHiddenFields(schema, args ?? {}))
})
$effect(() => {
isValid = allTrue(inputCheck ?? {})
})
const actions_render = $derived(actions)
</script>
<!-- {JSON.stringify(args)} -->
{#if showReset}
<div class="flex flex-row-reverse w-full">
<Button size="xs" color="light" on:click={() => setDefaults()}>
@@ -188,14 +257,12 @@
{/if}
<div
class="w-full {$$props.class} {flexWrap
? 'flex flex-row flex-wrap gap-x-6 '
: ''} {nestedClasses}"
class="w-full {className} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''} {nestedClasses}"
use:dragHandleZone={dndConfig ?? { items: [], dragDisabled: true }}
on:finalize
on:consider
onfinalize={bubble('finalize')}
onconsider={bubble('consider')}
>
{#if keys.length > 0}
{#if keys.length > 0 && args}
{#each fields as item, i (item.id)}
{@const argName = item.value}
<div
@@ -203,7 +270,7 @@
? 'bg-red-300 dark:bg-red-800 rounded-md'
: ''}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
{#if !hiddenArgs.includes(argName) && keys.includes(argName)}
{#if typeof diff[argName] === 'object' && diff[argName].oldSchema}
{@const formerProperty = diff[argName].oldSchema}
@@ -221,7 +288,8 @@
required={formerProperty?.required}
pattern={formerProperty?.pattern}
valid={inputCheck[argName]}
defaultValue={defaultValues?.[argName] ?? structuredClone(formerProperty?.default)}
defaultValue={defaultValues?.[argName] ??
structuredClone($state.snapshot(formerProperty?.default))}
enum_={dynamicEnums?.[argName] ?? formerProperty?.enum}
format={formerProperty?.format}
contentEncoding={formerProperty?.contentEncoding}
@@ -257,10 +325,10 @@
/>
</div>
{/if}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex flex-row items-center {largeGap ? 'pb-4' : ''} "
on:click={() => {
onclick={() => {
dispatch('click', argName)
}}
>
@@ -292,7 +360,7 @@
pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={defaultValues?.[argName] ??
structuredClone(schema.properties[argName].default)}
structuredClone($state.snapshot(schema.properties[argName].default))}
enum_={dynamicEnums?.[argName] ?? schema.properties[argName].enum}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
@@ -330,8 +398,8 @@
{css}
{displayType}
>
<svelte:fragment slot="actions">
<slot name="actions" />
{#snippet actions()}
{@render actions_render?.()}
{#if linkedSecretCandidates?.includes(argName)}
<div>
<ToggleButtonGroup
@@ -343,30 +411,31 @@
linkedSecret = undefined
}
}}
let:item
>
<ToggleButton
value="inlined"
size="sm"
label="Inlined"
tooltip="The value is inlined in the resource and thus has no special treatment."
{item}
/>
<ToggleButton
position="right"
value="secret"
size="sm"
label="Secret"
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
{item}
/>
{#snippet children({ item })}
<ToggleButton
value="inlined"
size="sm"
label="Inlined"
tooltip="The value is inlined in the resource and thus has no special treatment."
{item}
/>
<ToggleButton
position="right"
value="secret"
size="sm"
label="Secret"
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
{item}
/>
{/snippet}
</ToggleButtonGroup>
</div>{/if}</svelte:fragment
>
</div>{/if}
{/snippet}
</ArgInput>
{/if}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
{/if}
</div>
{/if}
@@ -380,7 +449,7 @@
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
if (pickForField && args) {
args[pickForField] = '$var:' + path
}
}}
@@ -394,17 +463,19 @@
...x
}))}
>
<div slot="submission">
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{#snippet submission()}
<div>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => variableEditor?.initNew?.()}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
@@ -94,7 +94,7 @@
<div class="h-fit">
<div class="relative z-[100000] w-full">
<div class="absolute" style="right: calc({100 - inputPanelSize}%); top: -1px;">
<SideBarTab expandRight={rightPanelOpen} {dropdownItems} fullMenu={true} noTrigger={true} />
<SideBarTab {dropdownItems} fullMenu={true} noTrigger={true} />
</div>
</div>
<Splitpanes class={!rightPanelOpen ? 'splitter-hidden' : ''}>
+45 -34
View File
@@ -13,7 +13,11 @@
import Button from './common/button/Button.svelte'
import { ClipboardCopy } from 'lucide-svelte'
export let schema: Schema | undefined | any = emptySchema()
interface Props {
schema?: Schema | undefined | any
}
let { schema = emptySchema() }: Props = $props()
function getProperties(schema: Schema) {
if (schema.properties) {
@@ -29,12 +33,13 @@
<Tabs selected="arguments">
<Tab value="arguments">Arguments</Tab>
<Tab value="advanced">Advanced</Tab>
<svelte:fragment slot="content">
{#snippet content()}
<div class="overflow-auto pt-2">
<TabContent value="arguments">
{#if schema && schema.properties && Object.keys(schema.properties).length > 0 && schema.required}
<div class="flex flex-row">
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row" class="underline">
<th>name</th>
<th>type</th>
@@ -43,37 +48,43 @@
<th>format</th>
<th>required</th>
</tr>
<tbody slot="body">
{#each getProperties(schema) as [name, property] (name)}
<tr>
<td class="font-semibold pl-1">{name}</td>
<td
><Badge color="blue"
>{#if !property.type} any {:else} {property.type} {/if}</Badge
></td
>
<td>{property.description ?? ''}</td>
<td
>{property.default == '<function call>'
? '<function call>'
: property.default
? JSON.stringify(property.default)
: ''}</td
>
<td
>{property.format ?? ''}
{property.contentEncoding
? `(encoding: ${property.contentEncoding})`
: ''}</td
>
<td
>{#if schema.required.includes(name)}
<span class="text-red-600 font-bold text-lg">*</span>
{/if}</td
>
</tr>
{/each}
</tbody>
{#snippet body()}
<tbody>
{#each getProperties(schema) as [name, property] (name)}
<tr>
<td class="font-semibold pl-1">{name}</td>
<td
><Badge color="blue"
>{#if !property.type}
any
{:else}
{property.type}
{/if}</Badge
></td
>
<td>{property.description ?? ''}</td>
<td
>{property.default == '<function call>'
? '<function call>'
: property.default
? JSON.stringify(property.default)
: ''}</td
>
<td
>{property.format ?? ''}
{property.contentEncoding
? `(encoding: ${property.contentEncoding})`
: ''}</td
>
<td
>{#if schema.required.includes(name)}
<span class="text-red-600 font-bold text-lg">*</span>
{/if}</td
>
</tr>
{/each}
</tbody>
{/snippet}
</TableCustom>
</div>
{:else}
@@ -96,6 +107,6 @@
</div>
</TabContent>
</div>
</svelte:fragment>
{/snippet}
</Tabs>
</div>
+194 -157
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import {
DraftService,
type NewScript,
@@ -69,7 +72,7 @@
import { writable } from 'svelte/store'
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import DefaultScripts from './DefaultScripts.svelte'
import { createEventDispatcher, onMount, setContext } from 'svelte'
import { createEventDispatcher, onMount, setContext, untrack } from 'svelte'
import Summary from './Summary.svelte'
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
@@ -95,24 +98,45 @@
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
import { Triggers } from './triggers/triggers.svelte'
export let script: NewScript & { draft_triggers?: Trigger[] }
export let fullyLoaded: boolean = true
export let initialPath: string = ''
export let template: 'docker' | 'bunnative' | 'script' = 'script'
export let initialArgs: Record<string, any> = {}
export let lockedLanguage = false
export let showMeta: boolean = false
export let neverShowMeta: boolean = false
export let diffDrawer: DiffDrawer | undefined = undefined
export let savedScript: NewScriptWithDraftAndDraftTriggers | undefined = undefined
export let searchParams: URLSearchParams = new URLSearchParams()
export let disableHistoryChange = false
export let replaceStateFn: (url: string) => void = (url) =>
window.history.replaceState(null, '', url)
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
export let functionExports: ((exports: ScriptBuilderFunctionExports) => void) | undefined =
undefined
interface Props {
script: NewScript & { draft_triggers?: Trigger[] }
fullyLoaded?: boolean
initialPath?: string
template?: 'docker' | 'bunnative' | 'script'
initialArgs?: Record<string, any>
lockedLanguage?: boolean
showMeta?: boolean
neverShowMeta?: boolean
diffDrawer?: DiffDrawer | undefined
savedScript?: NewScriptWithDraftAndDraftTriggers | undefined
searchParams?: URLSearchParams
disableHistoryChange?: boolean
replaceStateFn?: (url: string) => void
customUi?: ScriptBuilderWhitelabelCustomUi
savedPrimarySchedule?: ScheduleTrigger | undefined
functionExports?: ((exports: ScriptBuilderFunctionExports) => void) | undefined
children?: import('svelte').Snippet
}
let {
script = $bindable(),
fullyLoaded = true,
initialPath = $bindable(''),
template = $bindable('script'),
initialArgs = {},
lockedLanguage = false,
showMeta = false,
neverShowMeta = false,
diffDrawer = undefined,
savedScript = $bindable(undefined),
searchParams = new URLSearchParams(),
disableHistoryChange = false,
replaceStateFn = (url) => window.history.replaceState(null, '', url),
customUi = {},
savedPrimarySchedule = undefined,
functionExports = undefined,
children
}: Props = $props()
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
@@ -127,34 +151,35 @@
// used for new scripts for captures
const fakeInitialPath =
'u/' +
($userStore!.username?.includes('@')
? $userStore!.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
: $userStore!.username!) +
($userStore?.username?.includes('@')
? $userStore?.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
: $userStore?.username!) +
'/' +
generateRandomString(12)
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
let args: Record<string, any> = initialArgs // Test args input
let selectedInputTab: 'main' | 'preprocessor' = 'main'
let hasPreprocessor = false
let deployedValue: Value | undefined = $state(undefined) // Value to diff against
let deployedBy: string | undefined = $state(undefined) // Author
let confirmCallback: () => void = $state(() => {}) // What happens when user clicks `override` in warning
let open: boolean = $state(false) // Is confirmation modal open
let args: Record<string, any> = $state(initialArgs) // Test args input
let selectedInputTab: 'main' | 'preprocessor' = $state('main')
let hasPreprocessor = $state(false)
let metadataOpen =
let metadataOpen = $state(
!neverShowMeta &&
(showMeta ||
searchParams.get('metadata_open') == 'true' ||
(initialPath == '' &&
searchParams.get('state') == undefined &&
searchParams.get('collab') == undefined))
(showMeta ||
searchParams.get('metadata_open') == 'true' ||
(initialPath == '' &&
searchParams.get('state') == undefined &&
searchParams.get('collab') == undefined))
)
let editor: Editor | undefined = undefined
let scriptEditor: ScriptEditor | undefined = undefined
let captureTable: CaptureTable | undefined = undefined
let editor: Editor | undefined = $state(undefined)
let scriptEditor: ScriptEditor | undefined = $state(undefined)
let captureTable: CaptureTable | undefined = $state(undefined)
// Draft triggers confirmation modal
let draftTriggersModalOpen = false
let draftTriggersModalOpen = $state(false)
let confirmDeploymentCallback: (triggersToDeploy: Trigger[]) => void = () => {}
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
@@ -187,8 +212,6 @@
const dispatch = createEventDispatcher()
$: initialPath != '' && loadTriggers()
onMount(() => {
if (functionExports) {
console.log('functionExports set')
@@ -232,14 +255,16 @@
}
// Add triggers context store
const triggersState = new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'email', path: '', isDraft: false },
...(script.draft_triggers ?? [])
],
undefined,
saveSessionDraft
const triggersState = $state(
new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'email', path: '', isDraft: false },
...(script.draft_triggers ?? [])
],
undefined,
saveSessionDraft
)
)
const captureOn = writable<boolean | undefined>(undefined)
@@ -257,19 +282,6 @@
editor?.setCode(code)
}
$: langs = processLangs(
script.language,
$defaultScripts?.order ?? Object.keys(defaultScriptLanguages)
)
.map((l) => [defaultScriptLanguages[l], l])
.filter((x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1]))
.filter((x) => {
if (customUi?.settingsPanel?.metadata?.languages === undefined) {
return true
}
return customUi.settingsPanel.metadata.languages.includes(x[1] as SupportedLanguage)
}) as [string, SupportedLanguage | 'docker' | 'bunnative'][]
const scriptKindOptions: {
value: Script['kind']
title: string
@@ -312,19 +324,9 @@
}
]
let pathError = ''
let loadingSave = false
let loadingDraft = false
$: {
;['collab', 'path'].forEach((x) => {
if (searchParams.get(x)) {
searchParams.delete(x)
}
})
}
$: !disableHistoryChange && encodeScriptState(script)
let pathError = $state('')
let loadingSave = $state(false)
let loadingDraft = $state(false)
function encodeScriptState(script: NewScript) {
replaceStateFn(
@@ -557,7 +559,7 @@
)
}
const { draft_triggers: _, ...newScript } = structuredClone(script)
const { draft_triggers: _, ...newScript } = structuredClone($state.snapshot(script))
savedScript = structuredClone(newScript) as NewScriptWithDraft
setDraftTriggers([])
@@ -689,12 +691,13 @@
}
})
const clonedScript = structuredClone($state.snapshot(script))
savedScript = {
...(initialPath == '' || savedScript?.draft_only
? { ...structuredClone(script), draft_only: true }
? { ...clonedScript, draft_only: true }
: savedScript),
draft: {
...structuredClone(script),
...clonedScript,
draft_triggers: draftTriggers
}
} as NewScriptWithDraftAndDraftTriggers
@@ -793,25 +796,27 @@
}
}
let path: Path | undefined = undefined
let dirtyPath = false
let path: Path | undefined = $state(undefined)
let dirtyPath = $state(false)
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = (() => {
if (customUi?.settingsPanel?.disableMetadata !== true) {
// first option: either no custom UI or metadata is enabled
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = $state(
(() => {
if (customUi?.settingsPanel?.disableMetadata !== true) {
// first option: either no custom UI or metadata is enabled
return 'metadata'
}
if (customUi?.settingsPanel?.disableRuntime !== true) {
return 'runtime'
}
if (customUi?.settingsPanel?.disableGeneratedUi !== true) {
return 'ui'
}
if (customUi?.settingsPanel?.disableTriggers !== true) {
return 'triggers'
}
return 'metadata'
}
if (customUi?.settingsPanel?.disableRuntime !== true) {
return 'runtime'
}
if (customUi?.settingsPanel?.disableGeneratedUi !== true) {
return 'ui'
}
if (customUi?.settingsPanel?.disableTriggers !== true) {
return 'triggers'
}
return 'metadata'
})()
})()
)
setContext('disableTooltips', customUi?.disableTooltips === true)
@@ -913,10 +918,34 @@
)
}
}
$effect(() => {
initialPath != '' && untrack(() => loadTriggers())
})
let langs = $derived(
processLangs(script.language, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages))
.map((l) => [defaultScriptLanguages[l], l])
.filter((x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1]))
.filter((x) => {
if (customUi?.settingsPanel?.metadata?.languages === undefined) {
return true
}
return customUi.settingsPanel.metadata.languages.includes(x[1] as SupportedLanguage)
}) as [string, SupportedLanguage | 'docker' | 'bunnative'][]
)
$effect(() => {
;['collab', 'path'].forEach((x) => {
if (searchParams.get(x)) {
searchParams.delete(x)
}
})
})
$effect(() => {
!disableHistoryChange && untrack(() => encodeScriptState(script))
})
</script>
<svelte:window on:keydown={onKeyDown} />
<slot />
<svelte:window onkeydown={onKeyDown} />
{@render children?.()}
<DeployOverrideConfirmationModal
bind:deployedBy
@@ -949,7 +978,7 @@
aiId="script-builder-settings"
aiDescription="Script builder settings"
>
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<div class="flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="flex-none w-full">
{#if customUi?.settingsPanel?.disableMetadata !== true}
@@ -982,12 +1011,12 @@
</Tab>
{/if}
<svelte:fragment slot="content">
{#snippet content()}
<div class="min-h-0 grow overflow-y-auto">
<TabContent value="metadata">
<div class="flex flex-col gap-8 px-4 py-2">
<Section label="Metadata">
<svelte:fragment slot="action">
{#snippet action()}
{#if customUi?.settingsPanel?.metadata?.disableMute !== true}
<div class="flex flex-row items-center gap-2">
<ErrorHandlerToggleButton
@@ -998,7 +1027,7 @@
/>
</div>
{/if}
</svelte:fragment>
{/snippet}
<div class="flex flex-col gap-4">
<Label label="Summary">
<MetadataGen
@@ -1018,14 +1047,14 @@
/>
</Label>
<Label label="Path">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/roles_and_permissions#path"
>
The unique identifier of the script in the workspace that defines
permissions
</Tooltip>
</svelte:fragment>
{/snippet}
<Path
bind:this={path}
bind:error={pathError}
@@ -1060,7 +1089,9 @@
</Section>
<Section label="Language">
<svelte:fragment slot="action"><DefaultScripts /></svelte:fragment>
{#snippet action()}
<DefaultScripts />
{/snippet}
{#if lockedLanguage}
<div class="text-sm text-tertiary italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
@@ -1094,9 +1125,9 @@
<span class="text-tertiary !text-xs"> BETA </span>
{/if}
</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>
{/each}
</div>
@@ -1104,14 +1135,14 @@
{#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
<Section label="Script kind">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/script_kinds"
>
Tag this script's purpose within flows such that it is available as the
corresponding action.
</Tooltip>
</svelte:fragment>
{/snippet}
<ToggleButtonGroup
class="h-10"
selected={script.kind}
@@ -1120,19 +1151,20 @@
script.kind = detail
initContent(script.language, detail, template)
}}
let:item
>
{#each scriptKindOptions as { value, title, desc, documentationLink, Icon }}
<ToggleButton
label={title}
{value}
tooltip={desc}
{documentationLink}
icon={Icon}
showTooltipIcon={Boolean(desc)}
{item}
/>
{/each}
{#snippet children({ item })}
{#each scriptKindOptions as { value, title, desc, documentationLink, Icon }}
<ToggleButton
label={title}
{value}
tooltip={desc}
{documentationLink}
icon={Icon}
showTooltipIcon={Boolean(desc)}
{item}
/>
{/each}
{/snippet}
</ToggleButtonGroup>
</Section>
{/if}
@@ -1141,13 +1173,13 @@
<TabContent value="runtime">
<div class="flex flex-col gap-8 px-4 py-2">
<Section label="Concurrency limits" eeOnly>
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits"
>
Allowed concurrency within a given timeframe
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex flex-col gap-4">
<Label label="Max number of executions within the time window">
<div class="flex flex-row gap-2 max-w-sm">
@@ -1175,7 +1207,7 @@
/>
</Label>
<Label label="Custom concurrency key (optional)">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits#custom-concurrency-key"
>
@@ -1183,7 +1215,7 @@
using the variable `$workspace`. You can also use an argument's value
using `$args[name_of_arg]`</Tooltip
>
</svelte:fragment>
{/snippet}
<input
disabled={!$enterpriseLicense}
type="text"
@@ -1195,24 +1227,24 @@
</div>
</Section>
<Section label="Worker group tag (queue)">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>
The script will be executed on a worker configured to listen to this worker
group tag (queue). For instance, you could setup an "highmem", or "gpu" tag.
</Tooltip>
</svelte:fragment>
{/snippet}
<WorkerTagPicker bind:tag={script.tag} />
</Section>
<Section label="Cache">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/caching"
>
Cache the results for each possible inputs
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
@@ -1239,13 +1271,13 @@
</div>
</Section>
<Section label="Timeout">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/settings#timeout"
>
Add a custom timeout for this script
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
@@ -1270,13 +1302,13 @@
</div>
</Section>
<Section label="Perpetual script">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/perpetual_scripts"
>
Restart the script upon ending unless cancelled
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
@@ -1322,7 +1354,7 @@
</Alert>
</div>
{/if}
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/dedicated_workers"
>
@@ -1332,10 +1364,10 @@
languages, the efficiency is already on par with deidcated workers since
they do not spawn a full runtime</Tooltip
>
</svelte:fragment>
{/snippet}
</Section>
<Section label="Delete after use">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/settings#delete-after-use"
>
@@ -1355,7 +1387,7 @@
This option is only available on Windmill Enterprise Edition.
{/if}
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
disabled={!$enterpriseLicense}
@@ -1391,14 +1423,14 @@
right: 'Label as high priority'
}}
>
<svelte:fragment slot="right">
{#snippet right()}
<input
type="number"
class="!w-16 ml-4"
disabled={script.priority === undefined}
bind:value={script.priority}
on:focus
on:change={() => {
onfocus={bubble('focus')}
onchange={() => {
if (script.priority && script.priority > 100) {
script.priority = 100
} else if (script.priority && script.priority < 0) {
@@ -1406,9 +1438,9 @@
}
}}
/>
</svelte:fragment>
{/snippet}
</Toggle>
<svelte:fragment slot="header">
{#snippet header()}
<!-- TODO: Add EE-only badge when we have it -->
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/jobs#high-priority-jobs"
@@ -1418,11 +1450,11 @@
{#if !$enterpriseLicense}This is a feature only available on enterprise
edition.{/if}
</Tooltip>
</svelte:fragment>
{/snippet}
</Section>
{/if}
<Section label="Runs visibility">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs#invisible-runs"
>
@@ -1431,7 +1463,7 @@
setting can be overridden when this script is run manually from the advanced
menu.
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
@@ -1450,12 +1482,12 @@
</div>
</Section>
<Section label="On behalf of last editor">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
When this option is enabled, the script will be run with the permissions of
the last editor.
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
@@ -1475,13 +1507,13 @@
</Section>
{#if !isCloudHosted()}
<Section label="Custom env variables">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/custom_environment_variables"
>
Additional static custom env variables to pass to the script.
</Tooltip>
</svelte:fragment>
{/snippet}
{#if script.envs && script.envs.length > 0}
<Alert type="warning" title="Not passed in previews" size="xs">
Static envs variables are not passed in preview but solely on deployed
@@ -1493,14 +1525,20 @@
>Format is: `{'<KEY>=<VALUE>'}`</span
>
{#if Array.isArray(script.envs ?? [])}
{#each script.envs ?? [] as v, i}
{#each script.envs ?? [] as _v, i}
<div class="flex max-w-md mt-1 w-full items-center relative">
<input type="text" bind:value={v} placeholder="<KEY>=<VALUE>" />
{#if script.envs}
<input
type="text"
bind:value={script.envs[i]}
placeholder="<KEY>=<VALUE>"
/>
{/if}
<button
transition:fade|local={{ duration: 50 }}
class="rounded-full p-1 bg-surface/60 duration-200 hover:bg-gray-200 absolute right-2"
aria-label="Clear"
on:click={() => {
onclick={() => {
script.envs && script.envs.splice(i, 1)
script.envs = script.envs
}}
@@ -1566,7 +1604,7 @@
<!-- <ScriptSchedules {initialPath} schema={script.schema} schedule={scheduleStore} /> -->
</TabContent>
</div>
</svelte:fragment>
{/snippet}
</Tabs>
</div>
</DrawerContent>
@@ -1579,7 +1617,7 @@
<div class="center-center">
<button
disabled={customUi?.topBar?.settings == false}
on:click={async () => {
onclick={async () => {
metadataOpen = true
}}
>
@@ -1619,7 +1657,7 @@
<div>
{#if customUi?.topBar?.editablePath != false}
<button
on:click={async () => {
onclick={async () => {
metadataOpen = true
}}
>
@@ -1638,7 +1676,7 @@
value={script.path}
size={script.path?.length || 50}
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none !border-0 !shadow-none"
on:focus={({ currentTarget }) => {
onfocus={({ currentTarget }) => {
currentTarget.select()
}}
/>
@@ -1711,7 +1749,6 @@
stablePathForCaptures={initialPath || fakeInitialPath}
bind:code={script.content}
lang={script.language}
{initialArgs}
kind={script.kind}
{template}
tag={script.tag}
+165 -116
View File
@@ -13,7 +13,7 @@
import EditorBar, { EDITOR_BAR_WIDTH_THRESHOLD } from './EditorBar.svelte'
import TestJobLoader from './TestJobLoader.svelte'
import JobProgressBar from '$lib/components/jobs/JobProgressBar.svelte'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { createEventDispatcher, onDestroy, onMount, untrack } from 'svelte'
import { Button } from './common'
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
import WindmillIcon from './icons/WindmillIcon.svelte'
@@ -48,72 +48,111 @@
import { aiChatManager, AIMode } from './copilot/chat/AIChatManager.svelte'
import TriggerableByAI from './TriggerableByAI.svelte'
// Exported
export let schema: Schema | any = emptySchema()
export let code: string
export let path: string | undefined
export let lang: Preview['language']
export let kind: string | undefined = undefined
export let template: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' =
'script'
export let tag: string | undefined
export let initialArgs: Record<string, any> = {}
export let fixedOverflowWidgets = true
export let noSyncFromGithub = false
export let editor: Editor | undefined = undefined
export let diffEditor: DiffEditor | undefined = undefined
export let collabMode = false
export let edit = true
export let noHistory = false
export let saveToWorkspace = false
export let watchChanges = false
export let customUi: ScriptEditorWhitelabelCustomUi | undefined = undefined
export let args: Record<string, any> = initialArgs
export let selectedTab: 'main' | 'preprocessor' = 'main'
export let hasPreprocessor = false
export let captureTable: CaptureTable | undefined = undefined
export let showCaptures: boolean = true
export let stablePathForCaptures: string = ''
export let lastSavedCode: string | undefined = undefined
export let lastDeployedCode: string | undefined = undefined
interface Props {
// Exported
schema?: Schema | any
code: string
path: string | undefined
lang: Preview['language']
kind?: string | undefined
template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative'
tag: string | undefined
initialArgs?: Record<string, any>
fixedOverflowWidgets?: boolean
noSyncFromGithub?: boolean
editor?: Editor | undefined
diffEditor?: DiffEditor | undefined
collabMode?: boolean
edit?: boolean
noHistory?: boolean
saveToWorkspace?: boolean
watchChanges?: boolean
customUi?: ScriptEditorWhitelabelCustomUi | undefined
args: Record<string, any>
selectedTab?: 'main' | 'preprocessor'
hasPreprocessor?: boolean
captureTable?: CaptureTable | undefined
showCaptures?: boolean
stablePathForCaptures?: string
lastSavedCode?: string | undefined
lastDeployedCode?: string | undefined
editor_bar_right?: import('svelte').Snippet
}
let showHistoryDrawer = false
let {
schema = $bindable(),
code = $bindable(),
path,
lang,
kind = undefined,
template = 'script',
tag,
fixedOverflowWidgets = true,
noSyncFromGithub = false,
editor = $bindable(undefined),
diffEditor = $bindable(undefined),
collabMode = false,
edit = true,
noHistory = false,
saveToWorkspace = false,
watchChanges = false,
customUi = undefined,
args = $bindable(),
selectedTab = $bindable('main'),
hasPreprocessor = $bindable(false),
captureTable = $bindable(undefined),
showCaptures = true,
stablePathForCaptures = '',
lastSavedCode = undefined,
lastDeployedCode = undefined,
editor_bar_right
}: Props = $props()
let jobProgressReset: () => void
let diffMode = false
$effect.pre(() => {
if (schema == undefined) {
schema = emptySchema()
}
})
let showHistoryDrawer = $state(false)
let websocketAlive = {
let jobProgressReset: (() => void) | undefined = $state(undefined)
let diffMode = $state(false)
let websocketAlive = $state({
pyright: false,
deno: false,
go: false,
ruff: false,
shellcheck: false
}
})
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
$: watchChanges &&
(code != undefined || schema != undefined) &&
dispatchIfMounted('change', { code, schema })
$effect(() => {
watchChanges &&
(code != undefined || schema != undefined) &&
dispatchIfMounted('change', { code, schema })
})
let width = 1200
let width = $state(1200)
let testJobLoader: TestJobLoader
let testJobLoader: TestJobLoader | undefined = $state(undefined)
let isValid: boolean = true
let scriptProgress = undefined
let isValid: boolean = $state(true)
let scriptProgress = $state(undefined)
let logPanel: LogPanel | undefined = $state(undefined)
// Test
let testIsLoading = false
let testJob: Job | undefined
let pastPreviews: CompletedJob[] = []
let validCode = true
let testIsLoading = $state(false)
let testJob: Job | undefined = $state()
let pastPreviews: CompletedJob[] = $state([])
let validCode = $state(true)
let wsProvider: WebsocketProvider | undefined = undefined
let yContent: Y.Text | undefined = undefined
let peers: { name: string }[] = []
let showCollabPopup = false
let wsProvider: WebsocketProvider | undefined = $state(undefined)
let yContent: Y.Text | undefined = $state(undefined)
let peers: { name: string }[] = $state([])
let showCollabPopup = $state(false)
const url = new URL(window.location.toString())
let initialCollab = /true|1/i.test(url.searchParams.get('collab') ?? '0')
@@ -148,11 +187,11 @@
code,
lang,
selectedTab === 'preprocessor' || kind === 'preprocessor'
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args }
: args,
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...(args ?? {}) }
: (args ?? {}),
tag
)
setFocusToLogs()
logPanel?.setFocusToLogs()
return job
}
@@ -292,11 +331,15 @@
return `${url}?collab=1` + (edit ? '' : `&path=${path}`)
}
$: showTabs = hasPreprocessor
$: !hasPreprocessor && (selectedTab = 'main')
$: selectedTab && inferSchema(code)
let showTabs = $derived(hasPreprocessor)
$effect(() => {
!hasPreprocessor && (selectedTab = 'main')
})
$effect(() => {
selectedTab && code && untrack(() => inferSchema(code))
})
let argsRender = 0
let argsRender = $state(0)
export async function updateArgs(newArgs: Record<string, any>) {
if (Object.keys(newArgs).length > 0) {
args = { ...newArgs }
@@ -304,17 +347,17 @@
}
}
let setFocusToLogs = () => {}
setContext('disableTooltips', customUi?.disableTooltips === true)
let codePanelSize = 70
let testPanelSize = 30
let storedTestPanelSize = testPanelSize
let codePanelSize = $state(70)
let testPanelSize = $state(30)
let storedTestPanelSize = untrack(() => testPanelSize)
$: !SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '') &&
!aiChatManager.open &&
aiChatManager.toggleOpen()
$effect(() => {
if (SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '') == !aiChatManager.open) {
untrack(() => aiChatManager.toggleOpen())
}
})
function toggleTestPanel() {
if (testPanelSize > 0) {
@@ -348,26 +391,28 @@
editor?.show()
}
$: error = getError(testJob)
let error = $derived(getError(testJob))
$: {
$effect(() => {
const options: ScriptOptions = {
code,
lang: lang as ScriptLang,
error,
args,
args: args ?? {},
path,
lastSavedCode,
lastDeployedCode,
diffMode
}
aiChatManager.scriptEditorOptions = options
aiChatManager.scriptEditorApplyCode = (code: string) => {
hideDiffMode()
editor?.reviewAndApplyCode(code)
}
aiChatManager.scriptEditorShowDiffMode = showDiffMode
}
untrack(() => {
aiChatManager.scriptEditorOptions = options
aiChatManager.scriptEditorApplyCode = (code: string) => {
hideDiffMode()
editor?.reviewAndApplyCode(code)
}
aiChatManager.scriptEditorShowDiffMode = showDiffMode
})
})
</script>
<TestJobLoader
@@ -378,7 +423,7 @@
bind:job={testJob}
/>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<TriggerableByAI id="script-editor" description="Component to edit a script" />
@@ -397,42 +442,46 @@
</Modal>
<div class="border-b shadow-sm px-1 pr-4" bind:clientWidth={width}>
<div class="flex justify-between space-x-2">
<EditorBar
scriptPath={edit ? path : undefined}
on:toggleCollabMode={() => {
if (wsProvider?.shouldConnect) {
disableCollaboration()
} else {
setCollaborationMode()
}
}}
on:showDiffMode={showDiffMode}
on:hideDiffMode={hideDiffMode}
customUi={{ ...customUi?.editorBar, aiGen: false }}
collabLive={wsProvider?.shouldConnect}
{collabMode}
{validCode}
iconOnly={width < EDITOR_BAR_WIDTH_THRESHOLD}
on:collabPopup={() => (showCollabPopup = true)}
{editor}
{lang}
on:createScriptFromInlineScript
{websocketAlive}
collabUsers={peers}
kind={asKind(kind)}
{template}
{diffEditor}
{args}
{noHistory}
{saveToWorkspace}
lastDeployedCode={lastDeployedCode && lastDeployedCode !== code
? lastDeployedCode
: undefined}
{diffMode}
bind:showHistoryDrawer
>
<slot name="editor-bar-right" slot="right" />
</EditorBar>
{#if args}
<EditorBar
scriptPath={edit ? path : undefined}
on:toggleCollabMode={() => {
if (wsProvider?.shouldConnect) {
disableCollaboration()
} else {
setCollaborationMode()
}
}}
on:showDiffMode={showDiffMode}
on:hideDiffMode={hideDiffMode}
customUi={{ ...customUi?.editorBar, aiGen: false }}
collabLive={wsProvider?.shouldConnect}
{collabMode}
{validCode}
iconOnly={width < EDITOR_BAR_WIDTH_THRESHOLD}
on:collabPopup={() => (showCollabPopup = true)}
{editor}
{lang}
on:createScriptFromInlineScript
{websocketAlive}
collabUsers={peers}
kind={asKind(kind)}
{template}
{diffEditor}
{args}
{noHistory}
{saveToWorkspace}
lastDeployedCode={lastDeployedCode && lastDeployedCode !== code
? lastDeployedCode
: undefined}
{diffMode}
bind:showHistoryDrawer
>
{#snippet right()}
{@render editor_bar_right?.()}
{/snippet}
</EditorBar>
{/if}
{#if !noSyncFromGithub && customUi?.editorBar?.useVsCode != false}
<div class="py-1">
<Button
@@ -490,7 +539,7 @@
aiChatManager.toggleOpen()
}}
>
<svelte:fragment slot="popoverOverride">
{#snippet popoverOverride()}
<div class="text-sm">
Enable Windmill AI in the <a
href="{base}/workspace_settings?tab=ai"
@@ -500,7 +549,7 @@
workspace settings <ExternalLink size={16} />
</a>
</div>
</svelte:fragment>
{/snippet}
</HideButton>
{/if}
{/if}
@@ -670,7 +719,7 @@
</Pane>
<Pane size={67} class="relative">
<LogPanel
bind:setFocusToLogs
bind:this={logPanel}
{lang}
previewJob={testJob}
{pastPreviews}
@@ -690,7 +739,7 @@
compact={true}
/>
{/if}
<svelte:fragment slot="capturesTab">
{#snippet capturesTab()}
<div class="h-full p-2">
<CaptureTable
bind:this={captureTable}
@@ -704,7 +753,7 @@
on:addPreprocessor
/>
</div>
</svelte:fragment>
{/snippet}
</LogPanel>
</Pane>
</Splitpanes>
+40 -23
View File
@@ -3,7 +3,7 @@
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import Select from './Select.svelte'
@@ -19,21 +19,35 @@
import DarkModeObserver from './DarkModeObserver.svelte'
import { truncate } from '$lib/utils'
export let initialPath: string | undefined = undefined
export let scriptPath: string | undefined = undefined
export let allowFlow = false
export let itemKind: 'script' | 'flow' | 'app' = 'script'
export let kinds: Script['kind'][] = ['script']
export let disabled = false
export let allowRefresh = false
export let allowEdit = true
export let allowView = true
interface Props {
initialPath?: string | undefined
scriptPath?: string | undefined
allowFlow?: boolean
itemKind?: 'script' | 'flow' | 'app'
kinds?: Script['kind'][]
disabled?: boolean
allowRefresh?: boolean
allowEdit?: boolean
allowView?: boolean
}
let items: { value: string; label: string }[] = []
let drawerViewer: Drawer
let drawerFlowViewer: Drawer
let code: string = ''
let lang: SupportedLanguage | undefined
let {
initialPath = undefined,
scriptPath = $bindable(undefined),
allowFlow = false,
itemKind = $bindable('script'),
kinds = ['script'],
disabled = false,
allowRefresh = false,
allowEdit = true,
allowView = true
}: Props = $props()
let items: { value: string; label: string }[] = $state([])
let drawerViewer: Drawer | undefined = $state()
let drawerFlowViewer: Drawer | undefined = $state()
let code: string = $state('')
let lang: SupportedLanguage | undefined = $state()
let options: [[string, any, any, string | undefined]] = [['Script', 'script', Code2, undefined]]
allowFlow && options.push(['Flow', 'flow', FlowIcon, '#14b8a6'])
@@ -63,8 +77,10 @@
}
}
$: itemKind && $workspaceStore && loadItems()
let darkMode: boolean = false
$effect(() => {
itemKind && $workspaceStore && untrack(() => loadItems())
})
let darkMode: boolean = $state(false)
</script>
<DarkModeObserver bind:darkMode />
@@ -86,14 +102,15 @@
<div>
<ToggleButtonGroup
bind:selected={itemKind}
let:item
on:selected={() => {
scriptPath = ''
}}
>
{#each options as [label, value, icon, selectedColor]}
<ToggleButton {icon} {disabled} {value} {label} {selectedColor} {item} />
{/each}
{#snippet children({ item })}
{#each options as [label, value, icon, selectedColor]}
<ToggleButton {icon} {disabled} {value} {label} {selectedColor} {item} />
{/each}
{/snippet}
</ToggleButtonGroup>
</div>
{/if}
@@ -145,7 +162,7 @@
size="xs"
variant="border"
on:click={async () => {
drawerFlowViewer.openDrawer()
drawerFlowViewer?.openDrawer()
}}
>
View
@@ -203,7 +220,7 @@
const { language, content } = await getScriptByPath(scriptPath ?? '')
code = content
lang = language
drawerViewer.openDrawer()
drawerViewer?.openDrawer()
}}
>
View
@@ -4,8 +4,12 @@
import EditableSchemaForm from './EditableSchemaForm.svelte'
export let schema: Schema | any
export let customUi: EditableSchemaFormUi | undefined = undefined
interface Props {
schema: Schema | any
customUi?: EditableSchemaFormUi | undefined
}
let { schema = $bindable(), customUi = undefined }: Props = $props()
export function setSchema(newSchema: Schema) {
schema = newSchema
@@ -196,13 +196,14 @@
<div class="w-min">
<ToggleButtonGroup
selected={showDiff ? 'diff' : 'code'}
let:item
on:selected={({ detail }) => {
showDiff = detail === 'diff'
}}
>
<ToggleButton light small value="code" label="Code" icon={Code} {item} />
<ToggleButton light small value="diff" label="Diff" icon={Diff} {item} />
{#snippet children({ item })}
<ToggleButton light small value="code" label="Code" icon={Code} {item} />
<ToggleButton light small value="diff" label="Diff" icon={Diff} {item} />
{/snippet}
</ToggleButtonGroup>
</div>
+15 -8
View File
@@ -1,16 +1,18 @@
<script lang="ts">
import uFuzzy from '@leeoniya/ufuzzy'
import { untrack } from 'svelte'
export let filter: string = ''
export let items: any[] | undefined
export let f: (item: any) => string
export let filteredItems: (any & { marked: string })[] | undefined
export let opts: uFuzzy.Options = {}
interface Props {
filter?: string
items: any[] | undefined
f: (item: any) => string
filteredItems: (any & { marked: string })[] | undefined
opts?: uFuzzy.Options
}
let { filter = '', items, f, filteredItems = $bindable(), opts = {} }: Props = $props()
let uf = new uFuzzy(opts)
$: plaintextItems = items?.map((item) => f(item)) ?? []
$: plaintextItems && filter != undefined && setTimeout(() => filterItems(), 0)
function filterItems() {
let trimmed = filter.trim()
@@ -35,4 +37,9 @@
}
filteredItems = result
}
let plaintextItems = $derived(items?.map((item) => f(item)) ?? [])
$effect(() => {
plaintextItems && filter != undefined && setTimeout(() => untrack(() => filterItems()), 0)
})
</script>
+12 -2
View File
@@ -5,6 +5,7 @@
import ConditionalPortal from './common/drawer/ConditionalPortal.svelte'
import { deepEqual } from 'fast-equals'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
type Value = Item['value']
@@ -21,9 +22,11 @@
inputClass = '',
disablePortal = false,
loading = false,
autofocus,
groupBy,
sortBy,
onFocus,
onBlur,
onClear
}: {
items?: Item[]
@@ -38,9 +41,11 @@
inputClass?: string
disablePortal?: boolean
loading?: boolean
autofocus?: boolean
groupBy?: (item: Item) => string
sortBy?: (a: Item, b: Item) => number
onFocus?: () => void
onBlur?: () => void
onClear?: () => void
} = $props()
@@ -78,7 +83,9 @@
label: getLabel(item)
})) ?? []
if (filterText) {
items2 = items2.filter((item) => item.label.toLowerCase().includes(filterText.toLowerCase()))
items2 = items2.filter((item) =>
item?.label?.toLowerCase().includes(filterText?.toLowerCase())
)
}
if (groupBy) {
items2 =
@@ -134,7 +141,7 @@
if (!deepEqual(nPos, dropdownPos)) dropdownPos = nPos
if (open) requestAnimationFrame(updateDropdownPos)
}
if (open) updateDropdownPos()
if (open) untrack(() => updateDropdownPos())
})
</script>
@@ -164,6 +171,7 @@
use:clickOutside={{ onClickOutside: () => (open = false) }}
onpointerdown={() => onFocus?.()}
onfocus={() => onFocus?.()}
onblur={() => onBlur?.()}
>
{#if clearable && !disabled && value}
<div class="absolute z-10 right-2 h-full flex items-center">
@@ -175,7 +183,9 @@
<Loader2 size={20} class="animate-spin" />
</div>
{/if}
<!-- svelte-ignore a11y_autofocus -->
<input
{autofocus}
{disabled}
type="text"
bind:value={() => filterText, (v) => (filterText = v)}
@@ -399,7 +399,7 @@
<Drawer bind:this={logDrawer} bind:open={logDrawerOpen} size="1400px">
<DrawerContent title="See context" on:close={logDrawer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
on:click={() => copyToClipboard(content)}
color="light"
@@ -410,7 +410,7 @@
>
Copy to clipboard
</Button>
</svelte:fragment>
{/snippet}
<div class="w-fit">
<pre
class="bg-surface-secondary text-secondary text-xs w-full p-2 whitespace-pre border rounded-md"
+65 -59
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import TableCustom from './TableCustom.svelte'
import { GroupService, UserService, GranularAclService } from '$lib/gen'
@@ -34,22 +36,24 @@
| 'gcp_trigger'
let kind: Kind
let path: string = ''
let path: string = $state('')
let ownerKind: 'user' | 'group' = 'user'
let owner: string = ''
let ownerKind: 'user' | 'group' = $state('user')
let owner: string = $state('')
let newOwner: string = ''
let newOwner: string = $state('')
let write: boolean = false
let acls: [string, boolean][] = []
let groups: String[] = []
let usernames: string[] = []
let acls: [string, boolean][] = $state([])
let groups: String[] = $state([])
let usernames: string[] = $state([])
let drawer: Drawer
let drawer: Drawer | undefined = $state()
$: newOwner = [ownerKind === 'group' ? 'g' : 'u', owner].join('/')
run(() => {
newOwner = [ownerKind === 'group' ? 'g' : 'u', owner].join('/')
})
let own = false
let own = $state(false)
export async function openDrawer(newPath: string, kind_l: Kind) {
path = newPath
kind = kind_l
@@ -57,7 +61,7 @@
loadGroups()
loadUsernames()
loadOwner()
drawer.openDrawer()
drawer?.openDrawer()
}
async function loadOwner() {
@@ -106,7 +110,7 @@
</script>
<Drawer bind:this={drawer}>
<DrawerContent title="Share {path}" on:close={drawer.closeDrawer}>
<DrawerContent title="Share {path}" on:close={drawer?.closeDrawer}>
<div class="flex flex-col gap-6">
<h1>{path}</h1>
<h2
@@ -125,13 +129,11 @@
{#if own}
<div class="flex flex-row flex-wrap gap-2 items-center">
<div>
<ToggleButtonGroup
bind:selected={ownerKind}
on:selected={() => (owner = '')}
let:item
>
<ToggleButton value="user" size="xs" label="User" {item} />
<ToggleButton value="group" size="xs" label="Group" {item} />
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (owner = '')}>
{#snippet children({ item })}
<ToggleButton value="user" size="xs" label="User" {item} />
<ToggleButton value="group" size="xs" label="Group" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
{#key ownerKind}
@@ -146,52 +148,56 @@
</div>
{/if}
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>owner</th>
<th></th>
<th></th>
</tr>
<tbody slot="body">
{#each acls as [owner, write]}
<tr>
<td>{owner}</td>
<td
>{#if own}
<div>
<ToggleButtonGroup
selected={write ? 'writer' : 'viewer'}
on:selected={async (e) => {
const role = e.detail
if (role == 'writer') {
await addAcl(owner, true)
} else {
await addAcl(owner, false)
}
loadAcls()
}}
let:item
{#snippet body()}
<tbody>
{#each acls as [owner, write]}
<tr>
<td>{owner}</td>
<td
>{#if own}
<div>
<ToggleButtonGroup
selected={write ? 'writer' : 'viewer'}
on:selected={async (e) => {
const role = e.detail
if (role == 'writer') {
await addAcl(owner, true)
} else {
await addAcl(owner, false)
}
loadAcls()
}}
>
{#snippet children({ item })}
<ToggleButton value="viewer" size="xs" label="Viewer" {item} />
<ToggleButton value="writer" size="xs" label="Writer" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
{:else}{write}{/if}</td
>
<td>
{#if own}
<Button
variant="border"
color="red"
size="xs"
on:click={() => deleteAcl(owner)}
>
<ToggleButton value="viewer" size="xs" label="Viewer" {item} />
<ToggleButton value="writer" size="xs" label="Writer" {item} />
</ToggleButtonGroup>
</div>
{:else}{write}{/if}</td
>
<td>
{#if own}
<Button
variant="border"
color="red"
size="xs"
on:click={() => deleteAcl(owner)}
>
Delete
</Button>
{/if}
</td>
</tr>
{/each}
</tbody>
Delete
</Button>
{/if}
</td>
</tr>
{/each}
</tbody>
{/snippet}
</TableCustom>
</div>
</div>
@@ -2,6 +2,7 @@
import { sendUserToast } from '$lib/toast'
import { createGrid, type GridApi } from 'ag-grid-community'
import DarkModeObserver from './DarkModeObserver.svelte'
import { untrack } from 'svelte'
type Props = {
data: Record<string, any>[]
@@ -11,7 +12,7 @@
let api: GridApi<any> | undefined = $state()
let eGui: HTMLDivElement | undefined = $state()
$effect(() => eGui && mountGrid())
$effect(() => eGui && untrack(() => mountGrid()))
function mountGrid() {
if (eGui) {
createGrid(eGui, {
+30 -25
View File
@@ -69,7 +69,7 @@
import { allClasses } from './apps/editor/componentsPanel/cssUtils'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { createEventDispatcher, onDestroy, onMount, untrack } from 'svelte'
import libStdContent from '$lib/es6.d.ts.txt?raw'
import domContent from '$lib/dom.d.ts.txt?raw'
@@ -238,13 +238,13 @@
$effect(() => {
if (allowVim && editor !== null && $vimMode && statusDiv) {
onVimMode()
untrack(() => onVimMode())
}
})
$effect(() => {
if (!$vimMode && vimDisposable) {
onVimDisable()
untrack(() => onVimDisable())
}
})
@@ -303,7 +303,7 @@
$effect(() => {
if (editor !== null && (lang || disableLinting || disableSuggestions || hideLineNumbers)) {
updateModelAndOptions()
untrack(() => updateModelAndOptions())
}
})
@@ -352,26 +352,31 @@
if (!divEl) {
return
}
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
fontSize: fontSize,
quickSuggestions: disableSuggestions
? { other: false, comments: false, strings: false }
: { other: true, comments: true, strings: true },
suggestOnTriggerCharacters: !disableSuggestions,
wordBasedSuggestions: disableSuggestions ? 'off' : 'matchingDocuments',
parameterHints: { enabled: !disableSuggestions },
suggest: {
showIcons: !disableSuggestions,
showSnippets: !disableSuggestions,
showKeywords: !disableSuggestions,
showWords: !disableSuggestions,
snippetsPreventQuickSuggestions: disableSuggestions
}
})
try {
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
fontSize: fontSize,
quickSuggestions: disableSuggestions
? { other: false, comments: false, strings: false }
: { other: true, comments: true, strings: true },
suggestOnTriggerCharacters: !disableSuggestions,
wordBasedSuggestions: disableSuggestions ? 'off' : 'matchingDocuments',
parameterHints: { enabled: !disableSuggestions },
suggest: {
showIcons: !disableSuggestions,
showSnippets: !disableSuggestions,
showKeywords: !disableSuggestions,
showWords: !disableSuggestions,
snippetsPreventQuickSuggestions: disableSuggestions
}
})
} catch (e) {
console.error('Error loading monaco:', e)
return
}
keepModelAroundToAvoidDisposalOfWorkers()
let timeoutModel: NodeJS.Timeout | undefined = undefined
@@ -575,7 +580,7 @@
$effect(() => {
if (mounted && extraLib && initialized) {
loadExtraLib()
untrack(() => loadExtraLib())
}
})
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import type { EnumType } from '$lib/common'
import { computeKind } from '$lib/utils'
import Label from './Label.svelte'
@@ -12,28 +14,48 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import RegexGen from './copilot/RegexGen.svelte'
export let pattern: string | undefined
export let enum_: EnumType
export let format: string | undefined
export let contentEncoding: 'base64' | 'binary' | undefined
export let customErrorMessage: string | undefined
export let minRows: number | undefined = undefined
export let disableCreate: boolean | undefined = undefined
export let disableVariablePicker: boolean | undefined = undefined
export let password: boolean | undefined = undefined
export let noExtra = false
export let dateFormat: string | undefined
export let enumLabels: Record<string, string> | undefined = undefined
export let overrideAllowKindChange: boolean = true
export let originalType: string | undefined = undefined
interface Props {
pattern: string | undefined
enum_: EnumType
format: string | undefined
contentEncoding: 'base64' | 'binary' | undefined
customErrorMessage: string | undefined
minRows?: number | undefined
disableCreate?: boolean | undefined
disableVariablePicker?: boolean | undefined
password?: boolean | undefined
noExtra?: boolean
dateFormat: string | undefined
enumLabels?: Record<string, string> | undefined
overrideAllowKindChange?: boolean
originalType?: string | undefined
}
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' | 'date-time' =
let {
pattern = $bindable(),
enum_ = $bindable(),
format = $bindable(),
contentEncoding = $bindable(),
customErrorMessage = $bindable(),
minRows = $bindable(undefined),
disableCreate = $bindable(undefined),
disableVariablePicker = $bindable(undefined),
password = $bindable(undefined),
noExtra = false,
dateFormat = $bindable(),
enumLabels = $bindable(undefined),
overrideAllowKindChange = true,
originalType = undefined
}: Props = $props()
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' | 'date-time' = $state(
computeKind(enum_, contentEncoding, pattern, format)
)
const allowKindChange = overrideAllowKindChange || originalType === 'string'
let patternStr: string = pattern ?? ''
let resource: string | undefined
let patternStr: string = $state(pattern ?? '')
let resource: string | undefined = $state()
const FORMATS = [
'email',
@@ -60,16 +82,22 @@
['Pattern', 'pattern']
]
$: format =
kind == 'resource' ? (resource != undefined ? `resource-${resource}` : 'resource') : format
$: pattern = patternStr == '' ? undefined : patternStr
$: contentEncoding = kind == 'base64' ? 'base64' : undefined
run(() => {
format =
kind == 'resource' ? (resource != undefined ? `resource-${resource}` : 'resource') : format
})
run(() => {
pattern = patternStr == '' ? undefined : patternStr
})
run(() => {
contentEncoding = kind == 'base64' ? 'base64' : undefined
})
$: {
run(() => {
if (format == 'email') {
pattern = '^[\\w-+.]+@([\\w-]+\\.)+[\\w-]{2,63}$'
}
}
})
function add() {
if (enumLabels === undefined) {
@@ -133,27 +161,28 @@
disableVariablePicker = undefined
}
}}
let:item
>
{#each FIELD_SETTINGS as x}
<ToggleButton
value={x[1]}
label={x[0]}
tooltip={x[2]}
showTooltipIcon={Boolean(x[2])}
{item}
/>
{/each}
{#snippet children({ item })}
{#each FIELD_SETTINGS as x}
<ToggleButton
value={x[1]}
label={x[0]}
tooltip={x[2]}
showTooltipIcon={Boolean(x[2])}
{item}
/>
{/each}
{/snippet}
</ToggleButtonGroup>
{/if}
{#if kind == 'pattern'}
<Label label="Pattern (Regex)">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip light>
Setting a pattern will allow you to specify a regular expression that the input should
adhere to. You can use the regex generator to help you create a pattern.
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex flex-row gap-1">
<ClearableInput
id="input"
@@ -171,13 +200,13 @@
</div>
</Label>
<Label label="Custom error message" class="w-full">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip light>
Setting a custom error message will allow you to specify a message that will be shown when
the input does not match the pattern.
</Tooltip>
</svelte:fragment>
<svelte:fragment slot="action">
{/snippet}
{#snippet action()}
<Toggle
size="xs"
options={{ right: 'Enable' }}
@@ -190,7 +219,7 @@
}
}}
/>
</svelte:fragment>
{/snippet}
<input
type="text"
bind:value={customErrorMessage}
@@ -199,43 +228,45 @@
</Label>
{:else if kind == 'enum'}
<Label label="Enums">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip light>
Setting enums will allow you to specify a list of values that the input can take. If you
want to allow custom values, you can disable the option below.
</Tooltip>
</svelte:fragment>
{/snippet}
<div class="flex flex-col gap-1">
{#each enum_ || [] as e}
<div class="flex flex-row w-full gap-2 pt-2">
<input
id="input"
type="text"
bind:value={e}
on:input={(event) => onEnumKeyChange(event?.currentTarget.value, e)}
/>
{#if enumLabels !== undefined}
{#if enum_}
{#each enum_ as _, i}
<div class="flex flex-row w-full gap-2 pt-2">
<input
id="input"
type="text"
bind:value={enumLabels[e]}
placeholder="Optional title..."
on:input={(event) => {
if (event?.currentTarget.value === '') {
if (enumLabels === undefined) {
enumLabels = {}
}
delete enumLabels[e]
}
}}
bind:value={enum_[i]}
oninput={(event) => enum_ && onEnumKeyChange(event?.currentTarget.value, enum_[i])}
/>
{/if}
{#if enumLabels !== undefined}
<input
id="input"
type="text"
bind:value={enumLabels[enum_[i]]}
placeholder="Optional title..."
oninput={(event) => {
if (event?.currentTarget.value === '') {
if (enumLabels === undefined) {
enumLabels = {}
}
enum_ && delete enumLabels[enum_[i]]
}
}}
/>
{/if}
{#if allowKindChange}
<Button size="sm" on:click={() => remove(e)}>-</Button>
{/if}
</div>
{/each}
{#if allowKindChange}
<Button size="sm" on:click={() => enum_ && remove(enum_[i])}>-</Button>
{/if}
</div>
{/each}
{/if}
</div>
{#if allowKindChange}
<div class="flex flex-row my-1">
@@ -262,11 +293,11 @@
<ResourceTypePicker bind:value={resource} />
{:else if kind == 'format'}
<Label label="Format">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip light>
Setting the format will allow you to specify a format that the input should adhere to.
</Tooltip>
</svelte:fragment>
{/snippet}
<select bind:value={format}>
<option value={undefined}></option>
{#each FORMATS as f}
@@ -279,12 +310,12 @@
<div class="grid grid-cols-3 gap-2">
<Label label="Date format passed to script" class="col-span-2">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip light>
Setting the date output format will allow you to specify how the date will be passed
to the script.
</Tooltip>
</svelte:fragment>
{/snippet}
<ClearableInput type="text" bind:value={dateFormat} placeholder="yyyy-MM-dd" />
</Label>
<Label label="Presets">
@@ -175,12 +175,14 @@
>
<Highlight language={json} code={JSON.stringify(resourceValue, null, 4)} />
{/if}
<div slot="actions" class="flex gap-1">
{#if step == 'resource' && selectedDatabase != undefined}
<Button variant="border" on:click={() => (step = 'init')}>Back</Button>
{#snippet actions()}
<div class="flex gap-1">
{#if step == 'resource' && selectedDatabase != undefined}
<Button variant="border" on:click={() => (step = 'init')}>Back</Button>
<Button {disabled} on:click={save}>Save</Button>
{/if}
</div>
<Button {disabled} on:click={save}>Save</Button>
{/if}
</div>
{/snippet}
</DrawerContent>
</Drawer>
@@ -24,8 +24,9 @@
import InstanceNameEditor from './InstanceNameEditor.svelte'
import Toggle from './Toggle.svelte'
import { instanceSettingsSelectedTab } from '$lib/stores'
let drawer: Drawer
let filter = ''
let drawer: Drawer | undefined = $state()
let filter = $state('')
export function openDrawer() {
listUsers(activeOnly)
@@ -44,33 +45,39 @@
gotoUrl(hashRemoved)
}
let users: GlobalUserInfo[] = []
let filteredUsers: GlobalUserInfo[] = []
let deleteConfirmedCallback: (() => void) | undefined = undefined
let activeOnly = false
let users: GlobalUserInfo[] = $state([])
let filteredUsers: GlobalUserInfo[] = $state([])
let deleteConfirmedCallback: (() => void) | undefined = $state(undefined)
let activeOnly = $state(false)
async function listUsers(activeOnly: boolean): Promise<void> {
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000, activeOnly: activeOnly })
}
$: listUsers(activeOnly)
$effect(() => {
listUsers(activeOnly)
})
let tab: 'users' | string = 'users'
let tab: 'users' | string = $state('users')
$: $instanceSettingsSelectedTab, (tab = $instanceSettingsSelectedTab)
$: tab, instanceSettingsSelectedTab.set(tab)
$effect(() => {
tab = $instanceSettingsSelectedTab
})
$effect(() => {
instanceSettingsSelectedTab.set(tab)
})
let nbDisplayed = 50
let nbDisplayed = $state(50)
let instanceSettings
let instanceSettings: InstanceSettings | undefined = $state()
let automateUsernameCreation = false
let automateUsernameCreation = $state(false)
async function getAutomateUsernameCreationSetting() {
automateUsernameCreation =
((await SettingService.getGlobal({ key: 'automate_username_creation' })) as any) ?? false
}
getAutomateUsernameCreationSetting()
let automateUsernameModalOpen = false
let automateUsernameModalOpen = $state(false)
async function enableAutomateUsernameCreationSetting() {
await SettingService.setGlobal({
key: 'automate_username_creation',
@@ -145,7 +152,7 @@
{category}
</Tab>
{/each}
<svelte:fragment slot="content">
{#snippet content()}
<div class="pt-4"></div>
<TabContent value="users">
<div class="h-full">
@@ -210,6 +217,7 @@
</div>
<div class="mt-2 overflow-auto">
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row" class="sticky top-0 bg-surface border-b">
<th>email</th>
<th>auth</th>
@@ -223,147 +231,154 @@
<th></th>
<th></th>
</tr>
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only } (email)}
<tr class="border">
<td>{email}</td>
<td>{login_type}</td>
<td><span class="break-words">{truncate(name ?? '', 30)}</span></td>
{#snippet body()}
<tbody class="overflow-y-auto w-full h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only } (email)}
<tr class="border">
<td>{email}</td>
<td>{login_type}</td>
<td><span class="break-words">{truncate(name ?? '', 30)}</span></td>
{#if automateUsernameCreation}
{#if automateUsernameCreation}
<td>
{#if username}
{username}
{:else}
{#key filteredUsers.map((u) => u.username).join()}
<ChangeInstanceUsername
username=""
{email}
isConflict
on:renamed={() => {
listUsers(activeOnly)
}}
/>
{/key}
{/if}
</td>
{/if}
{#if activeOnly}
<td>
{#if operator_only}
Operator only
{:else}
Developer
{/if}
</td>
{/if}
<td>
{#if username}
{username}
{:else}
{#key filteredUsers.map((u) => u.username).join()}
<ChangeInstanceUsername
username=""
{email}
isConflict
on:renamed={() => {
listUsers(activeOnly)
}}
/>
{/key}
{/if}
</td>
{/if}
{#if activeOnly}
<td>
{#if operator_only}
Operator only
{:else}
Developer
{/if}
</td>
{/if}
<td>
<ToggleButtonGroup
selected={super_admin ? 'super_admin' : devops ? 'devops' : 'user'}
on:selected={async (e) => {
if (email == $userStore?.email) {
sendUserToast('You cannot demote yourself', true)
listUsers(activeOnly)
return
}
let role = e.detail
if (role === 'super_admin') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: true,
is_devops: false
}
})
}
if (role === 'devops') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: false,
is_devops: true
}
})
}
if (role === 'user') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: false,
is_devops: false
}
})
}
sendUserToast('User updated')
listUsers(activeOnly)
}}
let:item
>
<ToggleButton value={'user'} size="xs" label="User" {item} />
<ToggleButton
value={'devops'}
size="xs"
label="Devops"
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
{item}
/>
<ToggleButton
value={'super_admin'}
size="xs"
label="Superadmin"
{item}
/>
</ToggleButtonGroup>
</td>
<td>
<div class="flex flex-row gap-x-1 justify-end">
<InstanceNameEditor
{login_type}
value={name}
{username}
{email}
on:refresh={() => {
listUsers(activeOnly)
}}
on:save={(e) => {
updateName(e.detail, email)
}}
on:renamed={() => {
listUsers(activeOnly)
}}
{automateUsernameCreation}
/>
<Button
color="light"
variant="contained"
size="xs"
spacingSize="xs2"
btnClasses="text-red-500"
on:click={() => {
deleteConfirmedCallback = async () => {
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
<ToggleButtonGroup
selected={super_admin
? 'super_admin'
: devops
? 'devops'
: 'user'}
on:selected={async (e) => {
if (email == $userStore?.email) {
sendUserToast('You cannot demote yourself', true)
listUsers(activeOnly)
return
}
let role = e.detail
if (role === 'super_admin') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: true,
is_devops: false
}
})
}
if (role === 'devops') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: false,
is_devops: true
}
})
}
if (role === 'user') {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: false,
is_devops: false
}
})
}
sendUserToast('User updated')
listUsers(activeOnly)
}}
>
Remove
</Button>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
{#snippet children({ item })}
<ToggleButton value={'user'} size="xs" label="User" {item} />
<ToggleButton
value={'devops'}
size="xs"
label="Devops"
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
{item}
/>
<ToggleButton
value={'super_admin'}
size="xs"
label="Superadmin"
{item}
/>
{/snippet}
</ToggleButtonGroup>
</td>
<td>
<div class="flex flex-row gap-x-1 justify-end">
<InstanceNameEditor
{login_type}
value={name}
{username}
{email}
on:refresh={() => {
listUsers(activeOnly)
}}
on:save={(e) => {
updateName(e.detail, email)
}}
on:renamed={() => {
listUsers(activeOnly)
}}
{automateUsernameCreation}
/>
<Button
color="light"
variant="contained"
size="xs"
spacingSize="xs2"
btnClasses="text-red-500"
on:click={() => {
deleteConfirmedCallback = async () => {
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
listUsers(activeOnly)
}
}}
>
Remove
</Button>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
{/snippet}
</TableCustom>
</div>
{#if filteredUsers && filteredUsers?.length > 50 && nbDisplayed < filteredUsers.length}
<span class="text-xs"
>{nbDisplayed} users out of {filteredUsers.length}
<button class="ml-4" on:click={() => (nbDisplayed += 50)}>load 50 more</button
<button class="ml-4" onclick={() => (nbDisplayed += 50)}>load 50 more</button
></span
>
{/if}
@@ -378,7 +393,7 @@
{closeDrawer}
/>
</TabContent>
</svelte:fragment>
{/snippet}
</Tabs>
</div>
</div>
@@ -453,16 +453,21 @@
model.updateOptions(updateOptions)
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
// overflowWidgetsDomNode: widgets,
// lineNumbers: 'on',
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
fontSize,
suggestOnTriggerCharacters: true
})
try {
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
// overflowWidgetsDomNode: widgets,
// lineNumbers: 'on',
lineDecorationsWidth: 6,
lineNumbersMinChars: 2,
fontSize,
suggestOnTriggerCharacters: true
})
} catch (e) {
console.error('Error loading monaco:', e)
return
}
editor.onDidFocusEditorText(() => {
dispatch('focus')
@@ -608,16 +613,20 @@
let mounted = false
onMount(async () => {
if (BROWSER) {
if (loadAsync) {
setTimeout(async () => {
try {
if (BROWSER) {
if (loadAsync) {
setTimeout(async () => {
await loadMonaco()
mounted = true
}, 0)
} else {
await loadMonaco()
mounted = true
}, 0)
} else {
await loadMonaco()
mounted = true
}
}
} catch (e) {
console.error('Error loading monaco:', e)
}
})
@@ -3,12 +3,18 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { Building } from 'lucide-svelte'
export let selected: 'hub' | 'workspace' = 'hub'
interface Props {
selected?: 'hub' | 'workspace'
}
let { selected = $bindable('hub') }: Props = $props()
</script>
<div class="max-w-min">
<ToggleButtonGroup bind:selected let:item>
<ToggleButton value="hub" icon={WindmillIcon} label="Hub" {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" {item} />
<ToggleButtonGroup bind:selected>
{#snippet children({ item })}
<ToggleButton value="hub" icon={WindmillIcon} label="Hub" {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -3,13 +3,19 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { Building } from 'lucide-svelte'
export let selected: 'all' | 'hub' | 'workspace' = 'all'
interface Props {
selected?: 'all' | 'hub' | 'workspace'
}
let { selected = $bindable('all') }: Props = $props()
</script>
<div class="max-w-min">
<ToggleButtonGroup id="flow-editor-flow-providers" bind:selected let:item>
<ToggleButton value="all" label="All" light small {item} />
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" light small {item} />
<ToggleButtonGroup id="flow-editor-flow-providers" bind:selected>
{#snippet children({ item })}
<ToggleButton value="all" label="All" light small {item} />
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" light small {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -20,24 +20,24 @@
const dispatch = createEventDispatcher()
let path: string = ''
let path: string = $state('')
let variable: {
value: string
is_secret: boolean
description: string
} = {
} = $state({
value: '',
is_secret: true,
description: ''
}
let valid = true
})
let valid = $state(true)
let drawer: Drawer
let edit = false
let initialPath: string
let pathError = ''
let can_write = true
let drawer: Drawer | undefined = $state()
let edit = $state(false)
let initialPath: string = $state('')
let pathError = $state('')
let can_write = $state(true)
export function initNew(): void {
variable = {
@@ -49,7 +49,7 @@
initialPath = ''
path = ''
can_write = true
drawer.openDrawer()
drawer?.openDrawer()
}
export async function editVariable(edit_path: string): Promise<void> {
@@ -70,7 +70,7 @@
}
initialPath = edit_path
path = edit_path
drawer.openDrawer()
drawer?.openDrawer()
}
export async function loadVariable(path: string): Promise<void> {
@@ -85,7 +85,9 @@
const MAX_VARIABLE_LENGTH = 10000
$: valid = variable.value.length <= MAX_VARIABLE_LENGTH
$effect(() => {
valid = variable.value.length <= MAX_VARIABLE_LENGTH
})
async function createVariable(): Promise<void> {
await VariableService.createVariable({
@@ -99,7 +101,7 @@
})
sendUserToast(`Created variable ${path}`)
dispatch('create')
drawer.closeDrawer()
drawer?.closeDrawer()
}
async function updateVariable(): Promise<void> {
@@ -122,19 +124,19 @@
})
sendUserToast(`Updated variable ${initialPath}`)
dispatch('create')
drawer.closeDrawer()
drawer?.closeDrawer()
} catch (err) {
sendUserToast(`Could not update variable: ${err.body}`, true)
}
}
let editorKind: 'plain' | 'json' | 'yaml' = 'plain'
let editor: SimpleEditor | undefined = undefined
let editorKind: 'plain' | 'json' | 'yaml' = $state('plain')
let editor: SimpleEditor | undefined = $state(undefined)
</script>
<Drawer bind:this={drawer} size="900px">
<DrawerContent
title={edit ? `Update variable at ${initialPath}` : 'Add a variable'}
on:close={drawer.closeDrawer}
on:close={drawer?.closeDrawer}
>
<div class="flex flex-col gap-8">
{#if !can_write}
@@ -168,11 +170,11 @@
</div>
</Section>
<Section label="Variable value">
<svelte:fragment slot="header">
{#snippet header()}
<span class="text-sm text-tertiary mr-4 font-normal">
({variable.value.length}/{MAX_VARIABLE_LENGTH} characters)
</span>
</svelte:fragment>
{/snippet}
<div>
<div class="mb-1">
{#if edit && variable.is_secret}{#if $userStore?.operator}
@@ -183,10 +185,12 @@
>{/if}{/if}
</div>
<div class="flex flex-col gap-2">
<ToggleButtonGroup bind:selected={editorKind} let:item>
<ToggleButton value="plain" label="Plain" {item} />
<ToggleButton value="json" label="Json" {item} />
<ToggleButton value="yaml" label="YAML" {item} />
<ToggleButtonGroup bind:selected={editorKind}>
{#snippet children({ item })}
<ToggleButton value="plain" label="Plain" {item} />
<ToggleButton value="json" label="Json" {item} />
<ToggleButton value="yaml" label="YAML" {item} />
{/snippet}
</ToggleButtonGroup>
{#if editorKind == 'plain'}
<textarea
@@ -229,15 +233,11 @@
</div>
</Section>
<Section label="Description">
<textarea
rows="4"
use:autosize
bind:value={variable.description}
placeholder="Used for X"
<textarea rows="4" use:autosize bind:value={variable.description} placeholder="Used for X"
></textarea>
</Section>
</div>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
on:click={() => (edit ? updateVariable() : createVariable())}
disabled={!can_write || !valid || pathError != ''}
@@ -247,6 +247,6 @@
>
{edit ? 'Update' : 'Save'}
</Button>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
+143 -135
View File
@@ -22,26 +22,6 @@
import AutoscalingConfigEditor from './AutoscalingConfigEditor.svelte'
import TagsToListenTo from './TagsToListenTo.svelte'
export let name: string
export let config:
| undefined
| {
dedicated_worker?: string
worker_tags?: string[]
priority_tags?: Map<string, number>
cache_clear?: number
init_bash?: string
additional_python_paths?: string[]
pip_local_dependencies?: string[]
min_alive_workers_alert_threshold?: number
autoscaling?: AutoscalingConfig
}
export let activeWorkers: number
export let customTags: string[] | undefined
export let workers: [string, WorkerPing[]][]
$: vcpus_memory = computeVCpuAndMemory(workers)
function computeVCpuAndMemory(workers: [string, WorkerPing[]][]) {
let vcpus = 0
let memory = 0
@@ -70,7 +50,7 @@
pip_local_dependencies?: string[]
min_alive_workers_alert_threshold?: number
autoscaling?: AutoscalingConfig
} = {}
} = $state({})
function loadNConfig() {
nconfig = config
@@ -103,12 +83,11 @@
customEnvVars.sort((a, b) => (a.key < b.key ? -1 : 1))
}
let selectedPriorityTags: string[] = []
let customEnvVars: {
key: string
type: 'static' | 'dynamic'
value: string | undefined
}[] = []
}[] = $state([])
const aws_env_vars_preset = [
'AWS_REGION',
@@ -125,24 +104,41 @@
'SSL_CERT_DIR'
]
$: selected = nconfig?.dedicated_worker != undefined ? 'dedicated' : 'normal'
$: {
selectedPriorityTags = []
if (nconfig?.priority_tags !== undefined) {
for (const [tag, _] of Object.entries(nconfig?.priority_tags)) {
selectedPriorityTags.push(tag)
}
}
let workspaceTag = $state('')
interface Props {
name: string
config:
| undefined
| {
dedicated_worker?: string
worker_tags?: string[]
priority_tags?: Map<string, number>
cache_clear?: number
init_bash?: string
additional_python_paths?: string[]
pip_local_dependencies?: string[]
min_alive_workers_alert_threshold?: number
autoscaling?: AutoscalingConfig
}
activeWorkers: number
customTags: string[] | undefined
workers: [string, WorkerPing[]][]
defaultTagPerWorkspace?: boolean | undefined
}
let workspaceTag = ''
export let defaultTagPerWorkspace: boolean | undefined = undefined
let {
name,
config = $bindable(),
activeWorkers,
customTags,
workers,
defaultTagPerWorkspace = undefined
}: Props = $props()
let workspaces: Workspace[] = []
let workspaces: Workspace[] = $state([])
async function listWorkspaces() {
workspaces = await WorkspaceService.listWorkspacesAsSuperAdmin()
}
$: $superadmin && listWorkspaces()
const dispatch = createEventDispatcher()
@@ -150,12 +146,17 @@
await ConfigService.deleteConfig({ name: 'worker__' + name })
dispatch('reload')
}
let dirty = false
let dirtyCode = false
let openDelete = false
let openClean = false
let dirty = $state(false)
let dirtyCode = $state(false)
let openDelete = $state(false)
let openClean = $state(false)
let drawer: Drawer
let drawer: Drawer | undefined = $state()
let vcpus_memory = $derived(computeVCpuAndMemory(workers))
let selected = $derived(nconfig?.dedicated_worker != undefined ? 'dedicated' : 'normal')
$effect(() => {
$superadmin && listWorkspaces()
})
</script>
<ConfirmationModal
@@ -207,7 +208,7 @@
<Drawer bind:this={drawer} size="800px">
<DrawerContent
on:close={() => drawer.closeDrawer()}
on:close={() => drawer?.closeDrawer()}
title={$superadmin ? `Edit worker config '${name}'` : `Worker config '${name}'`}
>
{#if !$enterpriseLicense}
@@ -235,22 +236,23 @@
}
}}
class="mb-4"
let:item
>
<ToggleButton
position="left"
value="normal"
size="sm"
label="Any jobs within worker tags"
{item}
/>
<ToggleButton
position="dedicated"
value="dedicated"
size="sm"
label="Dedicated to a script/flow"
{item}
/>
{#snippet children({ item })}
<ToggleButton
position="left"
value="normal"
size="sm"
label="Any jobs within worker tags"
{item}
/>
<ToggleButton
position="dedicated"
value="dedicated"
size="sm"
label="Dedicated to a script/flow"
{item}
/>
{/snippet}
</ToggleButtonGroup>
{#if selected == 'normal'}
<Section label="Tags to listen to">
@@ -264,7 +266,6 @@
if (nconfig.priority_tags) {
delete nconfig.priority_tags[tag]
}
selectedPriorityTags = selectedPriorityTags.filter((t) => t != tag) ?? []
}}
bind:worker_tags={nconfig.worker_tags}
{customTags}
@@ -354,7 +355,7 @@
<div class="max-w mt-2 items-center gap-1 pt-2">
{#if nconfig?.worker_tags !== undefined && nconfig?.worker_tags.length > 0}
<Label label="High-priority tags">
<svelte:fragment slot="header">
{#snippet header()}
<Tooltip>
Jobs with the following high-priority tags will be picked up in priority by this
worker.
@@ -362,11 +363,11 @@
This is a feature only available in enterprise edition.
{/if}
</Tooltip>
</svelte:fragment>
{/snippet}
<Multiselect
outerDivClass="text-secondary !bg-surface-disabled !border-0"
disabled={!$enterpriseLicense}
bind:selected={selectedPriorityTags}
selected={Object.keys(nconfig?.priority_tags ?? {})}
onchange={(e) => {
if (e.type === 'add') {
if (nconfig.priority_tags) {
@@ -426,7 +427,7 @@
disabled={!$enterpriseLicense}
min="1"
bind:value={nconfig.min_alive_workers_alert_threshold}
on:change={(ev) => {
onchange={(ev) => {
dirty = true
}}
/>
@@ -441,7 +442,7 @@
disabled={!$superadmin}
placeholder="<workspace>:<script path>"
type="text"
on:change={() => {
onchange={() => {
dirtyCode = true
dirty = true
}}
@@ -471,35 +472,37 @@
>
<div class="flex flex-col gap-3 gap-y-2 pb-2 max-w">
<span class="text-sm text-primary">Additional Python Paths</span>
{#each nconfig.additional_python_paths ?? [] as additional_python_path, i}
<div class="flex gap-1 items-center">
<input
type="text"
disabled={!$superadmin}
placeholder="/path/to/python3.X/site-packages"
bind:value={additional_python_path}
/>
{#if $superadmin}
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
on:click={() => {
if (
nconfig.additional_python_paths === undefined ||
nconfig.additional_python_paths.length == 0
) {
return
}
nconfig.additional_python_paths.splice(i, 1)
nconfig.additional_python_paths = [...nconfig.additional_python_paths]
dirty = true
}}
>
<X size={14} />
</button>
{/if}
</div>
{/each}
{#if nconfig.additional_python_paths}
{#each nconfig.additional_python_paths as _, i}
<div class="flex gap-1 items-center">
<input
type="text"
disabled={!$superadmin}
placeholder="/path/to/python3.X/site-packages"
bind:value={nconfig.additional_python_paths![i]}
/>
{#if $superadmin}
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
onclick={() => {
if (
nconfig.additional_python_paths === undefined ||
nconfig.additional_python_paths.length == 0
) {
return
}
nconfig.additional_python_paths.splice(i, 1)
nconfig.additional_python_paths = [...nconfig.additional_python_paths]
dirty = true
}}
>
<X size={14} />
</button>
{/if}
</div>
{/each}
{/if}
{#if $superadmin}
<div class="flex">
<Button
@@ -522,35 +525,37 @@
{/if}
<span class="text-sm text-primary">PIP local dependencies</span>
{#each nconfig.pip_local_dependencies ?? [] as pip_local_dependency, i}
<div class="flex gap-1 items-center">
<input
disabled={!$superadmin}
type="text"
placeholder="httpx"
bind:value={pip_local_dependency}
/>
{#if $superadmin}
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
on:click={() => {
if (
nconfig.pip_local_dependencies === undefined ||
nconfig.pip_local_dependencies.length == 0
) {
return
}
nconfig.pip_local_dependencies.splice(i, 1)
nconfig.pip_local_dependencies = [...nconfig.pip_local_dependencies]
dirty = true
}}
>
<X size={14} />
</button>
{/if}
</div>
{/each}
{#if nconfig.pip_local_dependencies}
{#each nconfig.pip_local_dependencies as _, i}
<div class="flex gap-1 items-center">
<input
disabled={!$superadmin}
type="text"
placeholder="httpx"
bind:value={nconfig.pip_local_dependencies[i]}
/>
{#if $superadmin}
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
onclick={() => {
if (
nconfig.pip_local_dependencies === undefined ||
nconfig.pip_local_dependencies.length == 0
) {
return
}
nconfig.pip_local_dependencies.splice(i, 1)
nconfig.pip_local_dependencies = [...nconfig.pip_local_dependencies]
dirty = true
}}
>
<X size={14} />
</button>
{/if}
</div>
{/each}
{/if}
{#if $superadmin}
<div class="flex">
<Button
@@ -589,7 +594,7 @@
type="text"
placeholder="ENV_VAR_NAME"
bind:value={envvar.key}
on:keypress={(e) => {
onkeypress={(e) => {
dirty = true
}}
/>
@@ -603,10 +608,11 @@
envvar.value = undefined
}
}}
let:item
>
<ToggleButton value="dynamic" label="Dynamic" {item} />
<ToggleButton value="static" label="Static" {item} />
{#snippet children({ item })}
<ToggleButton value="dynamic" label="Dynamic" {item} />
<ToggleButton value="static" label="Static" {item} />
{/snippet}
</ToggleButtonGroup>
<input
type="text"
@@ -620,7 +626,7 @@
<button
class="rounded-full bg-surface/60 hover:bg-gray-200"
aria-label="Clear"
on:click={() => {
onclick={() => {
if (nconfig.env_vars_static?.[envvar.key] !== undefined) {
delete nconfig.env_vars_static[envvar.key]
}
@@ -717,12 +723,14 @@
<div class="mt-8"></div>
<Section label="Autoscaling" collapsable>
<div slot="header" class="ml-4 flex flex-row gap-2 items-center">
<Badge>Beta</Badge>
{#if nconfig.autoscaling?.enabled}
<Badge color="green">Enabled</Badge>
{/if}
</div>
{#snippet header()}
<div class="ml-4 flex flex-row gap-2 items-center">
<Badge>Beta</Badge>
{#if nconfig.autoscaling?.enabled}
<Badge color="green">Enabled</Badge>
{/if}
</div>
{/snippet}
<AutoscalingConfigEditor
on:dirty={() => (dirty = true)}
worker_tags={config?.worker_tags}
@@ -766,7 +774,7 @@
</div>
</div>
</Section>
<svelte:fragment slot="actions">
{#snippet actions()}
<div class="flex gap-4 items-center">
<div class="flex gap-2 items-center">
{#if dirty}
@@ -828,7 +836,7 @@
</Button>
</div>
</div>
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
@@ -848,7 +856,7 @@
on:click={() => {
dirty = false
loadNConfig()
drawer.openDrawer()
drawer?.openDrawer()
}}
startIcon={{ icon: config == undefined ? Plus : Settings }}
>
@@ -911,7 +919,7 @@
size="xs"
on:click={() => {
loadNConfig()
drawer.openDrawer()
drawer?.openDrawer()
}}
>
<div class="flex flex-row gap-1 items-center"> config </div>
@@ -14,17 +14,29 @@
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import { User } from 'lucide-svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let recomputeIds: string[] | undefined = undefined
export let extraQueryParams: Record<string, any> = {}
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let customCss: ComponentCustomCSS<'formcomponent'> | undefined = undefined
export let render: boolean
export let errorHandledByComponent: boolean | undefined = false
interface Props {
id: string
componentInput: AppInput | undefined
configuration: RichConfigurations
recomputeIds?: string[] | undefined
extraQueryParams?: Record<string, any>
horizontalAlignment?: 'left' | 'center' | 'right' | undefined
customCss?: ComponentCustomCSS<'formcomponent'> | undefined
render: boolean
errorHandledByComponent?: boolean | undefined
}
$: errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
let {
id,
componentInput,
configuration,
recomputeIds = undefined,
extraQueryParams = {},
horizontalAlignment = undefined,
customCss = undefined,
render,
errorHandledByComponent = $bindable(false)
}: Props = $props()
export const staticOutputs: string[] = ['loading', 'result', 'jobId']
@@ -37,9 +49,8 @@
jobId: undefined
})
const resolvedConfig = initConfig(
components['formcomponent'].initialData.configuration,
configuration
const resolvedConfig = $state(
initConfig(components['formcomponent'].initialData.configuration, configuration)
)
$componentControl[id] = {
@@ -57,16 +68,19 @@
}
}
let runnableComponent: RunnableComponent
let loading = false
let runnableComponent: RunnableComponent | undefined = $state()
let loading = $state(false)
$: noInputs =
let css = $state(initCss($app.css?.formcomponent, customCss))
let wrapper: RunnableWrapper | undefined = $state()
$effect(() => {
errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
})
let noInputs = $derived(
$stateId != undefined &&
(componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0)
let css = initCss($app.css?.formcomponent, customCss)
let wrapper: RunnableWrapper
(componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0)
)
</script>
{#each Object.keys(components['formcomponent'].initialData.configuration) as key (key)}
@@ -15,18 +15,31 @@
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let recomputeIds: string[] | undefined = undefined
export let extraQueryParams: Record<string, any> = {}
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let customCss: ComponentCustomCSS<'formbuttoncomponent'> | undefined = undefined
export let render: boolean
export let errorHandledByComponent: boolean | undefined = false
interface Props {
id: string
componentInput: AppInput | undefined
configuration: RichConfigurations
recomputeIds?: string[] | undefined
extraQueryParams?: Record<string, any>
horizontalAlignment?: 'left' | 'center' | 'right' | undefined
verticalAlignment?: 'top' | 'center' | 'bottom' | undefined
customCss?: ComponentCustomCSS<'formbuttoncomponent'> | undefined
render: boolean
errorHandledByComponent?: boolean | undefined
}
$: errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
let {
id,
componentInput,
configuration,
recomputeIds = undefined,
extraQueryParams = {},
horizontalAlignment = undefined,
verticalAlignment = undefined,
customCss = undefined,
render,
errorHandledByComponent = $bindable(false)
}: Props = $props()
const { app, worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
@@ -50,25 +63,28 @@
loading: false
})
let resolvedConfig = initConfig(
components['formbuttoncomponent'].initialData.configuration,
configuration
let resolvedConfig = $state(
initConfig(components['formbuttoncomponent'].initialData.configuration, configuration)
)
let runnableComponent: RunnableComponent
let runnableComponent: RunnableComponent | undefined = $state()
let errors: Record<string, string> = {}
$: errorsMessage = Object.values(errors)
.filter((x) => x != '')
.join('\n')
$: noInputs =
let css = $state(initCss($app?.css?.formbuttoncomponent, customCss))
let runnableWrapper: RunnableWrapper | undefined = $state()
let loading = $state(false)
let modal: AlwaysMountedModal | undefined = $state()
$effect(() => {
errorHandledByComponent = resolvedConfig?.onError?.selected !== 'errorOverlay'
})
let errorsMessage = $derived(
Object.values(errors)
.filter((x) => x != '')
.join('\n')
)
let noInputs = $derived(
componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0
let css = initCss($app?.css?.formbuttoncomponent, customCss)
let runnableWrapper: RunnableWrapper
let loading = false
let modal: AlwaysMountedModal
)
</script>
{#each Object.keys(css ?? {}) as key (key)}
@@ -1,5 +1,7 @@
<script lang="ts">
import { getContext, onDestroy } from 'svelte'
import { stopPropagation } from 'svelte/legacy'
import { getContext, onDestroy, untrack } from 'svelte'
import { initConfig, initOutput, selectId } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type {
@@ -22,12 +24,23 @@
import { userStore } from '$lib/stores'
import SchemaForm from '$lib/components/SchemaForm.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
export let render: boolean
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
interface Props {
id: string
componentInput: AppInput | undefined
initializing?: boolean | undefined
render: boolean
configuration: RichConfigurations
customCss?: ComponentCustomCSS<'schemaformcomponent'> | undefined
}
let {
id,
componentInput,
initializing = $bindable(undefined),
render,
configuration,
customCss = undefined
}: Props = $props()
const {
worldStore,
@@ -49,8 +62,8 @@
values: {}
})
let result: Schema | undefined = undefined
let args: Record<string, unknown> = !iterContext ? outputs?.values?.peak() ?? {} : {}
let result: Schema | undefined = $state(undefined)
let args: Record<string, unknown> = $state(!iterContext ? (outputs?.values?.peak() ?? {}) : {})
function handleArgsChange() {
const newArgs: Record<string, unknown> = {}
@@ -71,7 +84,7 @@
listInputs?.remove(id)
})
let schemaForm: SchemaForm
let schemaForm: SchemaForm | undefined = $state()
$componentControl[id] = {
setValue(nvalue: any) {
@@ -89,24 +102,15 @@
}
}
$: args && handleArgsChange()
let css = $state(initCss($app.css?.schemaformcomponent, customCss))
$: outputs.valid.set(valid)
let css = initCss($app.css?.schemaformcomponent, customCss)
const resolvedConfig = initConfig(
components['schemaformcomponent'].initialData.configuration,
configuration
const resolvedConfig = $state(
initConfig(components['schemaformcomponent'].initialData.configuration, configuration)
)
let valid = true
let valid = $state(true)
let previousDefault = resolvedConfig.defaultValues
$: resolvedConfig.defaultValues &&
!deepEqual(previousDefault, resolvedConfig.defaultValues) &&
onDefaultChange()
let previousDefault = $state(resolvedConfig.defaultValues)
function onDefaultChange() {
previousDefault = structuredClone(resolvedConfig.defaultValues)
@@ -120,6 +124,17 @@
const policy = computeWorkspaceS3FileInputPolicy()
return policy
}
$effect(() => {
args && untrack(() => handleArgsChange())
})
$effect(() => {
outputs.valid.set(valid)
})
$effect(() => {
resolvedConfig.defaultValues &&
!deepEqual(previousDefault, resolvedConfig.defaultValues) &&
onDefaultChange()
})
</script>
{#each Object.keys(components['schemaformcomponent'].initialData.configuration) as key (key)}
@@ -148,8 +163,10 @@
style={css?.container?.style}
>
<div
on:pointerdown|stopPropagation={(e) =>
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
onpointerdown={stopPropagation(
(e) =>
!$connectingInput.opened && selectId(e as PointerEvent, id, selectedComponent, $app)
)}
>
<SchemaForm
noVariablePicker
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { initConfig, initOutput } from '../../editor/appUtils'
@@ -19,13 +21,25 @@
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import { Menubar, Menu, MeltButton } from '$lib/components/meltComponents'
export let id: string
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'menucomponent'> | undefined = undefined
export let render: boolean
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let menuItems: (BaseAppComponent & ButtonComponent)[]
interface Props {
id: string
configuration: RichConfigurations
customCss?: ComponentCustomCSS<'menucomponent'> | undefined
render: boolean
horizontalAlignment?: 'left' | 'center' | 'right' | undefined
verticalAlignment?: 'top' | 'center' | 'bottom' | undefined
menuItems: (BaseAppComponent & ButtonComponent)[]
}
let {
id,
configuration,
customCss = undefined,
render,
horizontalAlignment = undefined,
verticalAlignment = undefined,
menuItems
}: Props = $props()
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
@@ -35,18 +49,14 @@
}
})
const resolvedConfig = initConfig(
components['menucomponent'].initialData.configuration,
configuration
const resolvedConfig = $state(
initConfig(components['menucomponent'].initialData.configuration, configuration)
)
let css = initCss($app.css?.menucomponent, customCss)
let css = $state(initCss($app.css?.menucomponent, customCss))
let beforeIconComponent: any
let afterIconComponent: any
$: resolvedConfig.beforeIcon && beforeIconComponent && handleBeforeIcon()
$: resolvedConfig.afterIcon && afterIconComponent && handleAfterIcon()
let beforeIconComponent: any = $state()
let afterIconComponent: any = $state()
async function handleBeforeIcon() {
if (resolvedConfig.beforeIcon) {
@@ -71,6 +81,12 @@
)
}
}
run(() => {
resolvedConfig.beforeIcon && beforeIconComponent && handleBeforeIcon()
})
run(() => {
resolvedConfig.afterIcon && afterIconComponent && handleAfterIcon()
})
</script>
<InitializeComponent {id} />
@@ -96,83 +112,85 @@
{#if render}
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<Menubar let:createMenu class={resolvedConfig.fillContainer ? 'w-full h-full' : ''}>
<Menu
{createMenu}
placement="bottom-end"
justifyEnd={false}
class={resolvedConfig.fillContainer ? 'w-full h-full' : ''}
usePointerDownOutside={true}
renderContent
>
<svelte:fragment slot="trigger" let:trigger>
<MeltButton meltElement={trigger} class="w-full h-full">
<Button
on:pointerdown={(e) => e.stopPropagation()}
btnClasses={twMerge(
css?.button?.class,
'wm-button',
'wm-menu-button',
resolvedConfig.fillContainer ? 'w-full h-full' : ''
)}
wrapperClasses={twMerge(
'wm-button-container',
'wm-menu-button-container',
resolvedConfig.fillContainer ? 'w-full h-full' : ''
)}
style={css?.button?.style}
size={resolvedConfig.size}
color={resolvedConfig.color}
nonCaptureEvent
>
<span class="truncate inline-flex gap-2 items-center">
{#if resolvedConfig.beforeIcon}
{#key resolvedConfig.beforeIcon}
<div class="min-w-4" bind:this={beforeIconComponent}></div>
{/key}
{/if}
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
<div>{resolvedConfig.label}</div>
{/if}
{#if resolvedConfig.afterIcon}
{#key resolvedConfig.afterIcon}
<div class="min-w-4" bind:this={afterIconComponent}></div>
{/key}
{/if}
</span>
</Button>
</MeltButton>
</svelte:fragment>
<Menubar class={resolvedConfig.fillContainer ? 'w-full h-full' : ''}>
{#snippet children({ createMenu })}
<Menu
{createMenu}
placement="bottom-end"
justifyEnd={false}
class={resolvedConfig.fillContainer ? 'w-full h-full' : ''}
usePointerDownOutside={true}
renderContent
>
{#snippet triggr({ trigger })}
<MeltButton meltElement={trigger} class="w-full h-full">
<Button
on:pointerdown={(e) => e.stopPropagation()}
btnClasses={twMerge(
css?.button?.class,
'wm-button',
'wm-menu-button',
resolvedConfig.fillContainer ? 'w-full h-full' : ''
)}
wrapperClasses={twMerge(
'wm-button-container',
'wm-menu-button-container',
resolvedConfig.fillContainer ? 'w-full h-full' : ''
)}
style={css?.button?.style}
size={resolvedConfig.size}
color={resolvedConfig.color}
nonCaptureEvent
>
<span class="truncate inline-flex gap-2 items-center">
{#if resolvedConfig.beforeIcon}
{#key resolvedConfig.beforeIcon}
<div class="min-w-4" bind:this={beforeIconComponent}></div>
{/key}
{/if}
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
<div>{resolvedConfig.label}</div>
{/if}
{#if resolvedConfig.afterIcon}
{#key resolvedConfig.afterIcon}
<div class="min-w-4" bind:this={afterIconComponent}></div>
{/key}
{/if}
</span>
</Button>
</MeltButton>
{/snippet}
<div class="flex flex-col w-full p-1 gap-2 max-h-[50vh] overflow-y-auto">
{#if menuItems.length > 0}
{#each menuItems as actionButton, actionIndex (actionButton?.id)}
{#if actionButton.type == 'buttoncomponent'}
<div
on:pointerup={() => {
outputs?.result.set({
latestButtonClicked: actionButton.id
})
}}
>
<AppButton
noInitialize
extraKey={'idx' + actionIndex}
render={true}
id={actionButton.id}
customCss={actionButton.customCss}
configuration={actionButton.configuration}
recomputeIds={actionButton.recomputeIds}
componentInput={actionButton.componentInput}
noWFull={false}
isMenuItem={true}
/>
</div>
{/if}
{/each}
{/if}
</div>
</Menu>
<div class="flex flex-col w-full p-1 gap-2 max-h-[50vh] overflow-y-auto">
{#if menuItems.length > 0}
{#each menuItems as actionButton, actionIndex (actionButton?.id)}
{#if actionButton.type == 'buttoncomponent'}
<div
onpointerup={() => {
outputs?.result.set({
latestButtonClicked: actionButton.id
})
}}
>
<AppButton
noInitialize
extraKey={'idx' + actionIndex}
render={true}
id={actionButton.id}
customCss={actionButton.customCss}
configuration={actionButton.configuration}
recomputeIds={actionButton.recomputeIds}
componentInput={actionButton.componentInput}
noWFull={false}
isMenuItem={true}
/>
</div>
{/if}
{/each}
{/if}
</div>
</Menu>
{/snippet}
</Menubar>
</AlignWrapper>
{/if}
@@ -45,7 +45,7 @@
<Drawer bind:this={insertDrawer} size="800px">
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer}>
<svelte:fragment slot="actions">
{#snippet actions()}
<Button
color="dark"
size="xs"
@@ -55,7 +55,7 @@
>
Insert
</Button>
</svelte:fragment>
{/snippet}
<InsertRow bind:args bind:isInsertable {columnDefs} {dbType} />
</DrawerContent>
</Drawer>
@@ -680,19 +680,3 @@
{/if}
</div>
</RunnableWrapper>
<!-- <Portal name="db-explorer">
<Drawer bind:this={insertDrawer} size="800px">
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer}>
<svelte:fragment slot="actions">
<Button color="dark" size="xs" on:click={insert} disabled={!isInsertable}>Insert</Button>
</svelte:fragment>
<InsertRow
bind:args
bind:isInsertable
columnDefs={resolvedConfig.columnDefs}
dbType={resolvedConfig.type.selected}
/>
</DrawerContent>
</Drawer>
</Portal> -->
@@ -15,13 +15,9 @@
import { argSigToJsonSchemaType } from '$lib/inferArgSig'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { untrack } from 'svelte'
export let args: Record<string, any> = {}
export let dbType: DbType = 'postgresql'
let schema: Schema | undefined = undefined
export let columnDefs: Array<ColumnDef & ColumnMetadata> = []
let schema: Schema | undefined = $state(undefined)
type FieldMetadata = {
type: string
@@ -32,28 +28,6 @@
nullable: 'YES' | 'NO'
}
$: fields = columnDefs
?.filter((t) => {
const shouldFilter = t.isidentity === ColumnIdentity.Always || t?.hideInsert === true
return !shouldFilter
})
.map((column) => {
const type = column.datatype
const name = column.field
const isPrimaryKey = column.isprimarykey
const defaultValue = column.defaultValueNull ? null : column.defaultUserValue
return {
type,
name,
isPrimaryKey,
defaultValue,
identity: column.isidentity,
nullable: column.isnullable
}
}) as FieldMetadata[] | undefined
function parseSQLArgs(field: string, dbType: DbType): string {
let rawType = ''
switch (dbType) {
@@ -141,18 +115,57 @@
} as Schema
}
$: builtSchema(fields ?? [], dbType)
export let isInsertable: boolean = false
$: if (schema) {
const requiredFields = schema.required ?? []
const filledFields = Object.keys(args).filter(
(key) => args[key] !== undefined && args[key] !== null && args[key] !== ''
)
isInsertable = requiredFields.every((field) => filledFields.includes(field))
interface Props {
args?: Record<string, any>
dbType?: DbType
columnDefs?: Array<ColumnDef & ColumnMetadata>
isInsertable?: boolean
}
let {
args = $bindable({}),
dbType = 'postgresql',
columnDefs = [],
isInsertable = $bindable(false)
}: Props = $props()
let fields = $derived(
columnDefs
?.filter((t) => {
const shouldFilter = t.isidentity === ColumnIdentity.Always || t?.hideInsert === true
return !shouldFilter
})
.map((column) => {
const type = column.datatype
const name = column.field
const isPrimaryKey = column.isprimarykey
const defaultValue = column.defaultValueNull ? null : column.defaultUserValue
return {
type,
name,
isPrimaryKey,
defaultValue,
identity: column.isidentity,
nullable: column.isnullable
}
}) as FieldMetadata[] | undefined
)
$effect(() => {
;[fields, dbType]
untrack(() => builtSchema(fields ?? [], dbType))
})
$effect(() => {
if (schema) {
const requiredFields = schema.required ?? []
const filledFields = Object.keys(args).filter(
(key) => args[key] !== undefined && args[key] !== null && args[key] !== ''
)
isInsertable = requiredFields.every((field) => filledFields.includes(field))
}
})
</script>
{#if schema}
@@ -6,7 +6,7 @@
import { classNames, defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { deepEqual } from 'fast-equals'
import { Bug } from 'lucide-svelte'
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
import { createEventDispatcher, getContext, onDestroy, onMount, untrack } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
import type {
@@ -30,37 +30,70 @@
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
// Component props
export let id: string
export let fields: AppInputs
export let runnable: Runnable
export let transformer: (InlineScript & { language: 'frontend' }) | undefined
export let extraQueryParams: Record<string, any> = {}
export let autoRefresh: boolean = true
export let result: any = undefined
export let forceSchemaDisplay: boolean = false
export let wrapperClass = ''
export let wrapperStyle = ''
export let render: boolean
export let outputs: {
result: Output<any>
loading: Output<boolean>
jobId?: Output<any> | undefined
interface Props {
// Component props
id: string
fields: AppInputs
runnable: Runnable
transformer: (InlineScript & { language: 'frontend' }) | undefined
extraQueryParams?: Record<string, any>
autoRefresh?: boolean
result?: any
forceSchemaDisplay?: boolean
wrapperClass?: string
wrapperStyle?: string
render: boolean
outputs: {
result: Output<any>
loading: Output<boolean>
jobId?: Output<any> | undefined
}
extraKey?: string
initializing?: boolean
recomputeOnInputChanged?: boolean
loading?: boolean
refreshOnStart?: boolean
recomputableByRefreshButton: boolean
errorHandledByComponent?: boolean
hideRefreshButton?: boolean
hasChildrens: boolean
allowConcurentRequests?: boolean
noInitialize?: boolean
overrideCallback?: (() => CancelablePromise<void>) | undefined
overrideAutoRefresh?: boolean
replaceCallback?: boolean
children?: import('svelte').Snippet
}
export let extraKey = ''
export let initializing: boolean = false
export let recomputeOnInputChanged: boolean = true
export let loading = false
export let refreshOnStart: boolean = false
export let recomputableByRefreshButton: boolean
export let errorHandledByComponent: boolean = false
export let hideRefreshButton: boolean = false
export let hasChildrens: boolean
export let allowConcurentRequests = false
export let noInitialize = false
export let overrideCallback: (() => CancelablePromise<void>) | undefined = undefined
export let overrideAutoRefresh: boolean = false
export let replaceCallback: boolean = false
let {
id,
fields,
runnable,
transformer,
extraQueryParams = {},
autoRefresh = true,
result = $bindable(undefined),
forceSchemaDisplay = false,
wrapperClass = '',
wrapperStyle = '',
render,
outputs,
extraKey = '',
initializing = false,
recomputeOnInputChanged = true,
loading = $bindable(false),
refreshOnStart = false,
recomputableByRefreshButton,
errorHandledByComponent = false,
hideRefreshButton = false,
hasChildrens,
allowConcurentRequests = false,
noInitialize = false,
overrideCallback = undefined,
overrideAutoRefresh = false,
replaceCallback = false,
children
}: Props = $props()
const {
worldStore,
@@ -98,12 +131,10 @@
args = value
}
let args: Record<string, any> | undefined = undefined
let runnableInputValues: Record<string, any> = {}
let args: Record<string, any> | undefined = $state(undefined)
let runnableInputValues: Record<string, any> = $state({})
let executeTimeout: NodeJS.Timeout | undefined = undefined
$: outputs.loading?.set(loading)
function setDebouncedExecute() {
executeTimeout && clearTimeout(executeTimeout)
executeTimeout = setTimeout(() => {
@@ -121,12 +152,9 @@
}
let lazyStaticValues = computeStaticValues()
let currentStaticValues = lazyStaticValues
let currentStaticValues = $state(lazyStaticValues)
let isBg = id.startsWith('bg_')
$: isBg && updateBgRuns(loading)
$: fields && (currentStaticValues = computeStaticValues())
$: currentStaticValues && refreshOnStaticChange()
function refreshOnStaticChange() {
if (!deepEqual(currentStaticValues, lazyStaticValues)) {
@@ -138,14 +166,6 @@
// $: sendUserToast('args' + JSON.stringify(runnableInputValues) + Boolean(extraQueryParams) || args)
// $: console.log(runnableInputValues)
let firstRefresh = true
$: (runnableInputValues || extraQueryParams || args) &&
resultJobLoader &&
refreshIfAutoRefresh('arg changed')
$: runnableInputValues && dispatchIfMounted('argsChanged')
$: refreshOn =
runnable && runnable.type === 'runnableByName' ? (runnable.inlineScript?.refreshOn ?? []) : []
function refreshIfAutoRefresh(src: 'arg changed' | 'static changed') {
// console.log(
@@ -179,7 +199,7 @@
}
}
let schemaForm: SchemaForm
let schemaForm: SchemaForm | undefined = $state()
export function invalidate(key: string, error: string) {
schemaForm?.invalidate(key, error)
@@ -194,14 +214,11 @@
}
// Test job internal state
let resultJobLoader: ResultJobLoader | undefined = undefined
let resultJobLoader: ResultJobLoader | undefined = $state(undefined)
let schemaStripped: Schema | undefined =
let schemaStripped: Schema | undefined = $state(
autoRefresh || forceSchemaDisplay ? emptySchema() : undefined
$: (autoRefresh || forceSchemaDisplay) &&
Object.keys(fields ?? {}).length > 0 &&
(schemaStripped = stripSchema(fields, $stateId))
)
function stripSchema(inputs: AppInputs, s: any): Schema {
if (inputs === undefined) {
@@ -668,9 +685,9 @@
}
})
let lastJobId: string | undefined = undefined
let lastJobId: string | undefined = $state(undefined)
let inputValues: Record<string, InputValue> = {}
let inputValues: Record<string, InputValue> = $state({})
function updateBgRuns(loading: boolean) {
if (loading) {
@@ -699,6 +716,36 @@
const policy = computeWorkspaceS3FileInputPolicy()
return policy
}
$effect(() => {
outputs.loading?.set(loading)
})
$effect(() => {
isBg && untrack(() => updateBgRuns(loading))
})
$effect(() => {
fields && untrack(() => (currentStaticValues = computeStaticValues()))
})
$effect(() => {
currentStaticValues && untrack(() => refreshOnStaticChange())
})
$effect(() => {
;(runnableInputValues || extraQueryParams || args) &&
resultJobLoader &&
untrack(() => refreshIfAutoRefresh('arg changed'))
})
let ignoreFirst = true
$effect(() => {
runnableInputValues && !ignoreFirst && dispatchIfMounted('argsChanged')
ignoreFirst = false
})
let refreshOn = $derived(
runnable && runnable.type === 'runnableByName' ? (runnable.inlineScript?.refreshOn ?? []) : []
)
$effect(() => {
;(autoRefresh || forceSchemaDisplay) &&
Object.keys(fields ?? {}).length > 0 &&
untrack(() => (schemaStripped = stripSchema(fields, $stateId)))
})
</script>
{#each Object.entries(fields ?? {}) as [key, v] (key)}
@@ -807,36 +854,38 @@
>
<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 overflow-auto">
An error occured, please contact the app author.
{#snippet text()}
<span>
<div class="bg-surface">
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2 overflow-auto">
An error occured, please contact the app author.
{#if $errorByComponent?.[id]?.error}
<div class="font-bold">{$errorByComponent[id].error}</div>
{/if}
{#if lastJobId}
<a
href={`/run/${lastJobId}?workspace=${workspace}`}
class="font-semibold text-red-800 underline"
target="_blank"
>
Job id: {lastJobId}
</a>
{/if}
</div>
</Alert>
</div>
</span>
{#if $errorByComponent?.[id]?.error}
<div class="font-bold">{$errorByComponent[id].error}</div>
{/if}
{#if lastJobId}
<a
href={`/run/${lastJobId}?workspace=${workspace}`}
class="font-semibold text-red-800 underline"
target="_blank"
>
Job id: {lastJobId}
</a>
{/if}
</div>
</Alert>
</div>
</span>
{/snippet}
</Popover>
</div>
<div class="block grow w-full max-h-full border border-red-30 relative">
<slot />
{@render children?.()}
</div>
{:else}
<div class="block grow w-full max-h-full">
<slot />
{@render children?.()}
</div>
{/if}
@@ -280,13 +280,13 @@
fields={componentInput.fields}
bind:result
runnable={componentInput.runnable}
hideRefreshButton={componentInput.hideRefreshButton ?? hideRefreshButton}
hideRefreshButton={hideRefreshButton ?? componentInput.hideRefreshButton}
transformer={componentInput.transformer}
{autoRefresh}
{overrideCallback}
{overrideAutoRefresh}
recomputableByRefreshButton={componentInput.autoRefresh ?? true}
bind:recomputeOnInputChanged={componentInput.recomputeOnInputChanged}
recomputeOnInputChanged={componentInput.recomputeOnInputChanged}
{id}
{extraQueryParams}
{forceSchemaDisplay}
@@ -1,6 +1,9 @@
<script lang="ts">
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
import { afterUpdate, onMount, setContext } from 'svelte'
import { onMount, setContext, untrack } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { Pane, Splitpanes } from 'svelte-splitpanes'
@@ -21,7 +24,7 @@
import { Alert, Button, Tab } from '$lib/components/common'
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Tabs from '$lib/components/common/tabs/TabsV2.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { classNames, encodeState, getModifierKey, sendUserToast } from '$lib/utils'
import AppPreview from './AppPreview.svelte'
@@ -53,32 +56,47 @@
import AppEditorBottomPanel from './AppEditorBottomPanel.svelte'
import panzoom from 'panzoom'
export let app: App
export let path: string
export let policy: Policy
export let summary: string
export let fromHub: boolean = false
export let diffDrawer: DiffDrawer | undefined = undefined
export let savedApp:
| {
value: App
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined = undefined
export let version: number | undefined = undefined
export let newApp: boolean = false
export let newPath: string | undefined = undefined
export let replaceStateFn: (path: string) => void = (path: string) =>
window.history.replaceState(null, '', path)
export let gotoFn: (path: string, opt?: Record<string, any> | undefined) => void = (
path: string,
opt?: Record<string, any>
) => window.history.pushState(null, '', path)
interface Props {
app: App
path: string
policy: Policy
summary: string
fromHub?: boolean
diffDrawer?: DiffDrawer | undefined
savedApp?:
| {
value: App
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined
version?: number | undefined
newApp?: boolean
newPath?: string | undefined
replaceStateFn?: (path: string) => void
gotoFn?: (path: string, opt?: Record<string, any> | undefined) => void
unsavedConfirmationModal?: import('svelte').Snippet<[any]>
}
let {
app,
path,
policy,
summary,
fromHub = false,
diffDrawer = undefined,
savedApp = $bindable(undefined),
version = undefined,
newApp = false,
newPath = undefined,
replaceStateFn = (path: string) => window.history.replaceState(null, '', path),
gotoFn = (path: string, opt?: Record<string, any>) => window.history.pushState(null, '', path),
unsavedConfirmationModal
}: Props = $props()
migrateApp(app)
@@ -116,7 +134,7 @@
>({})
const focusedGrid = writable<FocusedGrid | undefined>(undefined)
const pickVariableCallback: Writable<((path: string) => void) | undefined> = writable(undefined)
let context = {
let context = $state({
email: $userStore?.email,
groups: $userStore?.groups,
username: $userStore?.username,
@@ -127,10 +145,10 @@
mode: 'editor',
summary: $summaryStore,
author: policy.on_behalf_of_email
}
})
const darkMode: Writable<boolean> = writable(document.documentElement.classList.contains('dark'))
const worldStore = buildWorld(context)
const worldStore = buildWorld(untrack(() => context))
const previewTheme: Writable<string | undefined> = writable(undefined)
const initialized = writable({
initialized: false,
@@ -147,7 +165,6 @@
$secondaryMenuLeftStore.isOpen = false
let writablePath = writable(path)
$: path && onPathChange()
function onPathChange() {
writablePath.set(path)
@@ -221,8 +238,6 @@
let timeout: NodeJS.Timeout | undefined = undefined
$: $appStore && saveFrontendDraft()
function saveFrontendDraft() {
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
@@ -239,23 +254,9 @@
context = context
}
$: context.mode = $mode == 'dnd' ? 'editor' : 'viewer'
let selectedTab: 'insert' | 'settings' | 'css' = $state('insert')
$: width =
$breakpoint === 'sm' && $appStore?.mobileViewOnSmallerScreens !== false
? 'min-w-[400px] max-w-[656px]'
: `min-w-[710px] ${$appStore.fullscreen ? 'w-full' : 'max-w-7xl'}`
let selectedTab: 'insert' | 'settings' | 'css' = 'insert'
let befSelected: string | undefined = undefined
$: if ($selectedComponent?.[0] != befSelected) {
befSelected = $selectedComponent?.[0]
if ($selectedComponent?.[0] != undefined) {
onSelectedComponentChange()
}
}
let befSelected: string | undefined = $state(undefined)
function onSelectedComponentChange() {
selectedTab = 'settings'
@@ -299,13 +300,9 @@
}
}
let itemPicker: ItemPicker | undefined = undefined
let itemPicker: ItemPicker | undefined = $state(undefined)
$: if ($pickVariableCallback) {
itemPicker?.openDrawer()
}
let variableEditor: VariableEditor | undefined = undefined
let variableEditor: VariableEditor | undefined = $state(undefined)
setContext<ContextPanelContext>('ContextPanel', {
search: writable<string>(''),
@@ -317,12 +314,12 @@
$darkMode = document.documentElement.classList.contains('dark')
}
let runnablePanelSize = 30
let gridPanelSize = 70
let runnablePanelSize = $state(30)
let gridPanelSize = $state(70)
let leftPanelSize = 22
let centerPanelSize = 56
let rightPanelSize = 22
let leftPanelSize = $state(22)
let centerPanelSize = $state(56)
let rightPanelSize = $state(22)
let tmpRunnablePanelSize = -1
let tmpGridPanelSize = -1
@@ -334,13 +331,6 @@
let toggled = false
let cssToggled = false
// Animation logic for cssInput
$: animateCssInput($cssEditorOpen)
$: $cssEditorOpen &&
secondaryMenuLeft?.open(StylePanel, {
type: 'style'
})
function animateCssInput(cssEditorOpen: boolean) {
if (cssEditorOpen && !cssToggled) {
cssToggled = true
@@ -389,17 +379,13 @@
}
}
$: $cssEditorOpen && selectCss()
function selectCss() {
selectedTab !== 'css' && (selectedTab = 'css')
}
const cssId = 'wm-global-style'
$: addOrRemoveCss(true, $mode === 'preview')
let css: string | undefined = undefined
let css: string | undefined = $state(undefined)
let lastTheme: string | undefined = undefined
appStore.subscribe(async (currentAppStore) => {
@@ -420,8 +406,6 @@
}
})
$: updateCssContent(css, $previewTheme)
function addOrRemoveCss(isPremium: boolean, isPreview: boolean = false) {
const existingElement = document.getElementById(cssId)
@@ -449,15 +433,15 @@
}
}
let appEditorHeader: AppEditorHeader | undefined = undefined
let appEditorHeader: AppEditorHeader | undefined = $state(undefined)
export function triggerTutorial() {
appEditorHeader?.toggleTutorial()
}
let box
let box: HTMLElement | undefined = $state(undefined)
function parseScroll() {
$yTop = box?.scrollTop
$yTop = box?.scrollTop ?? 0
}
let mounted = false
@@ -484,13 +468,13 @@
parseScroll()
})
$: setGridPanelSize($componentActive)
let lastComponentActive = false
afterUpdate(() => {
$effect(() => {
if ($componentActive != lastComponentActive) {
box.scrollTop = $yTop
if (box) {
box.scrollTop = $yTop
}
lastComponentActive = $componentActive
}
})
@@ -505,13 +489,13 @@
}
}
let runnableJobEnterTimeout: NodeJS.Timeout | undefined = undefined
let stillInJobEnter = false
let runnableJobEnterTimeout: NodeJS.Timeout | undefined = $state(undefined)
let stillInJobEnter = $state(false)
let storedLeftPanelSize = 0
let storedRightPanelSize = 0
let storedBottomPanelSize = 0
let centerPanelWidth = 0
let centerPanelWidth = $state(0)
function hideLeftPanel(animate: boolean = false) {
storedLeftPanelSize = leftPanelSize
@@ -682,10 +666,6 @@
}
}
$: $connectingInput.opened, updatePannelInConnecting()
$: updateCursorStyle(!!$connectingInput.opened && !$panzoomActive)
function updateCursorStyle(disabled: boolean) {
if (disabled) {
// Select all elements that don't have data-connection-button and aren't children of elements with data-connection-button
@@ -705,7 +685,7 @@
}
let instance: any
let isModifierKeyPressed = false
let isModifierKeyPressed = $state(false)
function resetView() {
if (instance && box) {
instance.moveTo(0.5, 0)
@@ -772,17 +752,82 @@
isModifierKeyPressed = false
}
let mouseOverGridView = false
let handMode = false
let forceDeactivatePanzoom = false
let mouseOverGridView = $state(false)
let handMode = $state(false)
let forceDeactivatePanzoom = $state(false)
$: $panzoomActive =
(isModifierKeyPressed || handMode) &&
!forceDeactivatePanzoom &&
!$componentActive &&
mouseOverGridView
$effect(() => {
path && untrack(() => onPathChange())
})
$effect(() => {
$appStore && untrack(() => saveFrontendDraft())
})
$effect(() => {
context.mode = $mode == 'dnd' ? 'editor' : 'viewer'
})
let width = $derived(
$breakpoint === 'sm' && $appStore?.mobileViewOnSmallerScreens !== false
? 'min-w-[400px] max-w-[656px]'
: `min-w-[710px] ${$appStore.fullscreen ? 'w-full' : 'max-w-7xl'}`
)
$effect(() => {
if ($selectedComponent?.[0] != befSelected) {
befSelected = $selectedComponent?.[0]
if ($selectedComponent?.[0] != undefined) {
untrack(() => onSelectedComponentChange())
}
}
})
$effect(() => {
if ($pickVariableCallback) {
itemPicker?.openDrawer()
}
})
// Animation logic for cssInput
$effect(() => {
;[$cssEditorOpen]
untrack(() => animateCssInput($cssEditorOpen))
})
$effect(() => {
$cssEditorOpen &&
secondaryMenuLeft?.open(StylePanel, {
type: 'style'
})
})
$effect(() => {
$cssEditorOpen && untrack(() => selectCss())
})
$effect(() => {
;[$mode]
untrack(() => addOrRemoveCss(true, $mode === 'preview'))
})
$effect(() => {
;[css, $previewTheme]
untrack(() => updateCssContent(css, $previewTheme))
})
$effect(() => {
;[$componentActive]
untrack(() => setGridPanelSize($componentActive))
})
$effect(() => {
$connectingInput.opened, untrack(() => updatePannelInConnecting())
})
$effect(() => {
forceDeactivatePanzoom = isModifierKeyPressed && handMode
})
$effect(() => {
$panzoomActive =
(isModifierKeyPressed || handMode) &&
!forceDeactivatePanzoom &&
!$componentActive &&
mouseOverGridView
})
$effect(() => {
;[!!$connectingInput.opened, !$panzoomActive]
untrack(() => updateCursorStyle(!!$connectingInput.opened && !$panzoomActive))
})
$: forceDeactivatePanzoom = isModifierKeyPressed && handMode
const unsavedConfirmationModal_render = $derived(unsavedConfirmationModal)
</script>
<svelte:head></svelte:head>
@@ -790,10 +835,10 @@
<DarkModeObserver on:change={onThemeChange} />
<svelte:window
on:hashchange={hashchange}
on:keydown={keydown}
on:keyup={handleKeyUp}
on:focus={() => {
onhashchange={hashchange}
onkeydown={keydown}
onkeyup={handleKeyUp}
onfocus={() => {
if (isModifierKeyPressed) {
isModifierKeyPressed = false
}
@@ -823,19 +868,17 @@
on:hideBottomPanel={() => hideBottomPanel()}
on:showBottomPanel={() => showBottomPanel()}
>
<svelte:fragment
slot="unsavedConfirmationModal"
let:diffDrawer
let:additionalExitAction
let:getInitialAndModifiedValues
>
<slot
name="unsavedConfirmationModal"
{diffDrawer}
{additionalExitAction}
{getInitialAndModifiedValues}
/>
</svelte:fragment>
{#snippet unsavedConfirmationModal({
diffDrawer,
additionalExitAction,
getInitialAndModifiedValues
})}
{@render unsavedConfirmationModal_render?.({
diffDrawer,
additionalExitAction,
getInitialAndModifiedValues
})}
{/snippet}
</AppEditorHeader>
{#if $mode === 'preview'}
<SplitPanesWrapper>
@@ -902,7 +945,7 @@
<Splitpanes id="o2" horizontal class="!overflow-visible">
<Pane bind:size={gridPanelSize} class="ovisible">
<div
on:pointerdown={(e) => {
onpointerdown={(e) => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
@@ -949,7 +992,7 @@
<div
class="absolute bottom-2 left-2 z-50 border bg-surface"
data-connection-button
on:pointerdown|stopPropagation
onpointerdown={stopPropagation(bubble('pointerdown'))}
>
<div class="flex flex-row gap-2 text-xs items-center h-8 px-1">
<Button
@@ -979,7 +1022,7 @@
<Scan size={14} />
</Button>
<Popover disappearTimeout={0} notClickable placement="bottom">
<svelte:fragment slot="text">
{#snippet text()}
<div class="flex flex-row gap-2">
<div class="flex-1">
Hand Mode
@@ -1003,7 +1046,7 @@
<span class="float-left text-tertiary-inverse">esc</span>
</div>
</div>
</svelte:fragment>
{/snippet}
<Button
color="light"
size="xs2"
@@ -1026,14 +1069,14 @@
class="absolute pointer-events-none inset-0 h-full w-full surface-secondary bg-[radial-gradient(#dbdbdb_1px,transparent_1px)] dark:bg-[radial-gradient(#666666_1px,transparent_1px)] [background-size:16px_16px]"
></div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={box}
on:scroll={parseScroll}
on:mouseenter={() => {
onscroll={parseScroll}
onmouseenter={() => {
mouseOverGridView = true
}}
on:mouseleave={() => {
onmouseleave={() => {
mouseOverGridView = false
}}
class={classNames(
@@ -1125,7 +1168,9 @@
<div bind:clientWidth={$runnableJob.width} class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="!min-h-[42px]" class="!h-full">
<Popover disappearTimeout={0} notClickable placement="bottom">
<svelte:fragment slot="text">Component library</svelte:fragment>
{#snippet text()}
Component library
{/snippet}
<Tab
value="insert"
size="xs"
@@ -1144,7 +1189,9 @@
</Tab>
</Popover>
<Popover disappearTimeout={0} notClickable placement="bottom">
<svelte:fragment slot="text">Component settings</svelte:fragment>
{#snippet text()}
Component settings
{/snippet}
<Tab
value="settings"
size="xs"
@@ -1162,7 +1209,9 @@
</Tab>
</Popover>
<Popover disappearTimeout={0} notClickable placement="bottom">
<svelte:fragment slot="text">Global styling</svelte:fragment>
{#snippet text()}
Global styling
{/snippet}
<Tab
value="css"
size="xs"
@@ -1182,29 +1231,31 @@
<div class="h-full w-full flex justify-end px-1">
<HideButton on:click={() => hideRightPanel()} direction="right" />
</div>
<div slot="content" class="h-full overflow-y-auto">
<TabContent class="overflow-auto h-full" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel
on:delete={() => {
befSelected = undefined
selectedTab = 'insert'
}}
/>
<SecondaryMenu right />
{:else}
<div class="min-w-[150px] text-sm !text-secondary text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
<TabContent value="css" class="h-full">
<CssSettings />
</TabContent>
</div>
{#snippet content()}
<div class="h-full overflow-y-auto">
<TabContent class="overflow-auto h-full" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel
on:delete={() => {
befSelected = undefined
selectedTab = 'insert'
}}
/>
<SecondaryMenu right />
{:else}
<div class="min-w-[150px] text-sm !text-secondary text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
<TabContent value="css" class="h-full">
<CssSettings />
</TabContent>
</div>
{/snippet}
</Tabs>
</div>
</Pane>
@@ -1232,22 +1283,23 @@
...x
}))}
>
<div
slot="submission"
class="flex flex-row-reverse w-full bg-surface border-t border-gray-200 rounded-bl-lg rounded-br-lg"
>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew?.()
}}
{#snippet submission()}
<div
class="flex flex-row-reverse w-full bg-surface border-t border-gray-200 rounded-bl-lg rounded-br-lg"
>
New variable
</Button>
</div>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew?.()
}}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
@@ -1,4 +1,7 @@
<script lang="ts">
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import { Alert, Drawer, DrawerContent, UndoRedo } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
@@ -27,7 +30,7 @@
SunMoon,
Zap
} from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import {
cleanValueProperties,
orderedJsonStringify,
@@ -95,32 +98,49 @@
}
}
export let policy: Policy
export let fromHub: boolean = false
export let diffDrawer: DiffDrawer | undefined = undefined
export let savedApp:
| {
value: App
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined = undefined
export let version: number | undefined = undefined
export let leftPanelHidden: boolean = false
export let rightPanelHidden: boolean = false
export let bottomPanelHidden: boolean = false
export let newApp: boolean
export let newPath: string = ''
interface Props {
policy: Policy
fromHub?: boolean
diffDrawer?: DiffDrawer | undefined
savedApp?:
| {
value: App
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined
version?: number | undefined
leftPanelHidden?: boolean
rightPanelHidden?: boolean
bottomPanelHidden?: boolean
newApp: boolean
newPath?: string
unsavedConfirmationModal?: import('svelte').Snippet<[any]>
}
let newEditedPath = ''
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
let {
policy = $bindable(),
fromHub = false,
diffDrawer = undefined,
savedApp = $bindable(undefined),
version = $bindable(undefined),
leftPanelHidden = false,
rightPanelHidden = false,
bottomPanelHidden = false,
newApp,
newPath = '',
unsavedConfirmationModal
}: Props = $props()
let newEditedPath = $state('')
let deployedValue: Value | undefined = $state(undefined) // Value to diff against
let deployedBy: string | undefined = $state(undefined) // Author
let confirmCallback: () => void = $state(() => {}) // What happens when user clicks `override` in warning
let open: boolean = $state(false) // Is confirmation modal open
const {
app,
@@ -139,30 +159,24 @@
const { history, jobsDrawerOpen, refreshComponents } =
getContext<AppEditorContext>('AppEditorContext')
const loading = {
const loading = $state({
publish: false,
save: false,
saveDraft: false
}
})
$: if ($openDebugRun == undefined) {
$openDebugRun = (jobId: string) => {
$jobsDrawerOpen = true
selectedJobId = jobId
}
}
let selectedJobId: string | undefined = undefined
let selectedJobId: string | undefined = $state(undefined)
let pathError: string | undefined = undefined
let appExport: AppExportButton
let pathError: string | undefined = $state(undefined)
let appExport: AppExportButton | undefined = $state()
let draftDrawerOpen = false
let saveDrawerOpen = false
let inputsDrawerOpen = fromHub
let historyBrowserDrawerOpen = false
let debugAppDrawerOpen = false
let lazyDrawerOpen = false
let deploymentMsg: string | undefined = undefined
let draftDrawerOpen = $state(false)
let saveDrawerOpen = $state(false)
let inputsDrawerOpen = $state(fromHub)
let historyBrowserDrawerOpen = $state(false)
let debugAppDrawerOpen = $state(false)
let lazyDrawerOpen = $state(false)
let deploymentMsg: string | undefined = $state(undefined)
function closeSaveDrawer() {
saveDrawerOpen = false
@@ -497,9 +511,7 @@
}
}
let secretUrl: string | undefined = undefined
$: $appPath && $appPath != '' && secretUrl == undefined && getSecretUrl()
let secretUrl: string | undefined = $state(undefined)
async function getSecretUrl() {
secretUrl = await AppService.getPublicSecretOfApp({
@@ -680,7 +692,7 @@
}
}
let onLatest = true
let onLatest = $state(true)
async function compareVersions() {
if (version === undefined) {
return
@@ -697,10 +709,6 @@
}
}
$: saveDrawerOpen && compareVersions()
$: hasErrors = Object.keys($errorByComponent).length > 0
let lock = false
function onKeyDown(event: KeyboardEvent) {
if (lock) return
@@ -757,8 +765,8 @@
lock = false
}
let dirtyPath = false
let path: Path | undefined = undefined
let dirtyPath = $state(false)
let path: Path | undefined = $state(undefined)
let moreItems = [
{
@@ -773,7 +781,7 @@
displayName: 'Export',
icon: FileJson,
action: () => {
appExport.open($app)
appExport?.open($app)
}
},
// {
@@ -788,7 +796,7 @@
displayName: 'Hub compatible JSON',
icon: FileUp,
action: () => {
appExport.open(toStatic($app, $staticExporter, $summary).app)
appExport?.open(toStatic($app, $staticExporter, $summary).app)
}
},
{
@@ -850,13 +858,13 @@
}
]
let appEditorTutorial: AppEditorTutorial | undefined = undefined
let appEditorTutorial: AppEditorTutorial | undefined = $state(undefined)
export function toggleTutorial() {
appEditorTutorial?.toggleTutorial()
}
let appReportingDrawerOpen = false
let appReportingDrawerOpen = $state(false)
export function openTroubleshootPanel() {
debugAppDrawerOpen = true
@@ -879,12 +887,9 @@
let priorDarkMode = document.documentElement.classList.contains('dark')
setTheme($app?.darkMode)
let customPath = savedApp?.custom_path
let dirtyCustomPath = false
let customPathError = ''
$: fullCustomUrl = `${window.location.origin}${base}/a/${
isCloudHosted() ? $workspaceStore + '/' : ''
}${customPath}`
let customPath = $state(savedApp?.custom_path)
let dirtyCustomPath = $state(false)
let customPathError = $state('')
async function appExists(customPath: string) {
return await AppService.customPathExists({
workspace: $workspaceStore!,
@@ -908,19 +913,41 @@
validateTimeout = undefined
}, 500)
}
$: customPath !== undefined && validateCustomPath(customPath)
$effect(() => {
if ($openDebugRun == undefined) {
$openDebugRun = (jobId: string) => {
$jobsDrawerOpen = true
selectedJobId = jobId
}
}
})
$effect(() => {
$appPath && $appPath != '' && secretUrl == undefined && untrack(() => getSecretUrl())
})
$effect(() => {
saveDrawerOpen && untrack(() => compareVersions())
})
let hasErrors = $derived(Object.keys($errorByComponent).length > 0)
let fullCustomUrl = $derived(
`${window.location.origin}${base}/a/${
isCloudHosted() ? $workspaceStore + '/' : ''
}${customPath}`
)
$effect(() => {
;[customPath]
untrack(() => customPath !== undefined && validateCustomPath(customPath))
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
{#if $$slots.unsavedConfirmationModal}
<slot
name="unsavedConfirmationModal"
{diffDrawer}
additionalExitAction={() => {
{#if unsavedConfirmationModal}
{@render unsavedConfirmationModal?.({
diffDrawer,
additionalExitAction: () => {
setTheme(priorDarkMode)
}}
getInitialAndModifiedValues={() => ({
},
getInitialAndModifiedValues: () => ({
savedValue: savedApp,
modifiedValue: {
summary: $summary,
@@ -929,8 +956,8 @@
policy,
custom_path: customPath
}
})}
/>
})
})}
{/if}
<DeployOverrideConfirmationModal
bind:deployedBy
@@ -955,15 +982,15 @@
</Alert>
<h3>Summary</h3>
<div class="w-full pt-2">
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus
type="text"
placeholder="App summary"
class="text-sm w-full font-semibold"
on:keydown|stopPropagation
onkeydown={stopPropagation(bubble('keydown'))}
bind:value={$summary}
on:keyup={() => {
onkeyup={() => {
if ($appPath == '' && $summary?.length > 0 && !dirtyPath) {
path?.setName(
$summary
@@ -989,15 +1016,17 @@
/>
<div class="py-4"></div>
<div slot="actions">
<Button
startIcon={{ icon: Save }}
disabled={pathError != ''}
on:click={() => saveInitialDraft()}
>
Save initial draft
</Button>
</div>
{#snippet actions()}
<div>
<Button
startIcon={{ icon: Save }}
disabled={pathError != ''}
on:click={() => saveInitialDraft()}
>
Save initial draft
</Button>
</div>
{/snippet}
</DrawerContent>
</Drawer>
{/if}
@@ -1028,15 +1057,15 @@
{/if}
<span class="text-secondary text-sm font-bold">Summary</span>
<div class="w-full pt-2">
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus
type="text"
placeholder="App summary"
class="text-sm w-full"
bind:value={$summary}
on:keydown|stopPropagation
on:keyup={() => {
onkeydown={stopPropagation(bubble('keydown'))}
onkeyup={() => {
if ($appPath == '' && $summary?.length > 0 && !dirtyPath) {
path?.setName(
$summary
@@ -1052,7 +1081,7 @@
<div class="py-4"></div>
<span class="text-secondary text-sm font-bold">Deployment message</span>
<div class="w-full pt-2">
<!-- svelte-ignore a11y-autofocus -->
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
placeholder="Optional deployment message"
@@ -1073,63 +1102,65 @@
autofocus={false}
/>
<div slot="actions" class="flex flex-row gap-4">
<Button
variant="border"
color="light"
disabled={!savedApp || savedApp.draft_only}
on:click={async () => {
if (!savedApp) {
return
}
// deployedValue should be syncronized when we open Diff
await syncWithDeployed()
{#snippet actions()}
<div class="flex flex-row gap-4">
<Button
variant="border"
color="light"
disabled={!savedApp || savedApp.draft_only}
on:click={async () => {
if (!savedApp) {
return
}
// deployedValue should be syncronized when we open Diff
await syncWithDeployed()
saveDrawerOpen = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: deployedValue ?? savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
value: $app,
path: newEditedPath || savedApp.draft?.path || savedApp.path,
policy,
custom_path: customPath
},
button: {
text: 'Looks good, deploy',
onClick: () => {
if ($appPath == '') {
createApp(newEditedPath)
} else {
handleUpdateApp(newEditedPath)
saveDrawerOpen = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: deployedValue ?? savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
value: $app,
path: newEditedPath || savedApp.draft?.path || savedApp.path,
policy,
custom_path: customPath
},
button: {
text: 'Looks good, deploy',
onClick: () => {
if ($appPath == '') {
createApp(newEditedPath)
} else {
handleUpdateApp(newEditedPath)
}
}
}
})
}}
>
<div class="flex flex-row gap-2 items-center">
<DiffIcon size={14} />
Diff
</div>
</Button>
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' || customPathError != ''}
on:click={() => {
if ($appPath == '') {
createApp(newEditedPath)
} else {
handleUpdateApp(newEditedPath)
}
})
}}
>
<div class="flex flex-row gap-2 items-center">
<DiffIcon size={14} />
Diff
</div>
</Button>
<Button
startIcon={{ icon: Save }}
disabled={pathError != '' || customPathError != ''}
on:click={() => {
if ($appPath == '') {
createApp(newEditedPath)
} else {
handleUpdateApp(newEditedPath)
}
}}
>
Deploy
</Button>
</div>
}}
>
Deploy
</Button>
</div>
{/snippet}
<div class="py-2"></div>
{#if $appPath == ''}
<Alert title="Require saving" type="error">
@@ -1219,7 +1250,7 @@
class={customPathError === ''
? ''
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
on:input={() => {
oninput={() => {
dirtyCustomPath = true
}}
/>
@@ -1296,22 +1327,23 @@
on:selected={({ detail }) => {
$app.fullscreen = detail === 'true'
}}
let:item
>
<ToggleButton
icon={AlignHorizontalSpaceAround}
value={'false'}
tooltip="The max width is 1168px and the content stay centered instead of taking the full page width"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="The width is of the app if the full width of its container"
icon={Expand}
value={'true'}
iconProps={{ size: 16 }}
{item}
/>
{#snippet children({ item })}
<ToggleButton
icon={AlignHorizontalSpaceAround}
value={'false'}
tooltip="The max width is 1168px and the content stay centered instead of taking the full page width"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="The width is of the app if the full width of its container"
icon={Expand}
value={'true'}
iconProps={{ size: 16 }}
{item}
/>
{/snippet}
</ToggleButtonGroup>
{/if}
{#if $app}
@@ -1322,60 +1354,63 @@
setTheme(theme)
}}
selected={$app.darkMode === undefined ? 'auto' : $app.darkMode ? 'dark' : 'sun'}
let:item
>
<ToggleButton
icon={SunMoon}
value={'auto'}
tooltip="The app mode between dark/light is automatic"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
icon={Sun}
value={'sun'}
tooltip="Force light mode"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="Force dark mode"
icon={Moon}
value={'dark'}
iconProps={{ size: 16 }}
{item}
/>
{#snippet children({ item })}
<ToggleButton
icon={SunMoon}
value={'auto'}
tooltip="The app mode between dark/light is automatic"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
icon={Sun}
value={'sun'}
tooltip="Force light mode"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="Force dark mode"
icon={Moon}
value={'dark'}
iconProps={{ size: 16 }}
{item}
/>
{/snippet}
</ToggleButtonGroup>
{/if}
<div class="flex flex-row gap-2">
<ToggleButtonGroup class="h-[30px]" bind:selected={$breakpoint} let:item>
<ToggleButton
tooltip="Computer View"
icon={Laptop2}
value={'lg'}
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="Mobile View"
icon={Smartphone}
value={'sm'}
iconProps={{ size: 16 }}
{item}
/>
{#if $breakpoint === 'sm'}
<Toggle
size="xs"
options={{
right: 'Enable mobile view for smaller screens',
rightTooltip:
'Desktop view is enabled by default. Enable this to customize the layout of the components for the mobile view'
}}
textClass="text-2xs whitespace-nowrap white !w-full"
bind:checked={$app.mobileViewOnSmallerScreens}
class="flex flex-row px-2 items-center"
<ToggleButtonGroup class="h-[30px]" bind:selected={$breakpoint}>
{#snippet children({ item })}
<ToggleButton
tooltip="Computer View"
icon={Laptop2}
value={'lg'}
iconProps={{ size: 16 }}
{item}
/>
{/if}
<ToggleButton
tooltip="Mobile View"
icon={Smartphone}
value={'sm'}
iconProps={{ size: 16 }}
{item}
/>
{#if $breakpoint === 'sm'}
<Toggle
size="xs"
options={{
right: 'Enable mobile view for smaller screens',
rightTooltip:
'Desktop view is enabled by default. Enable this to customize the layout of the components for the mobile view'
}}
textClass="text-2xs whitespace-nowrap white !w-full"
bind:checked={$app.mobileViewOnSmallerScreens}
class="flex flex-row px-2 items-center"
/>
{/if}
{/snippet}
</ToggleButtonGroup>
</div>
</div>
@@ -34,7 +34,7 @@
<Tabs bind:selected={rawType}>
<Tab value="yaml">YAML</Tab>
<Tab value="json">JSON</Tab>
<svelte:fragment slot="content">
{#snippet content()}
<div class="relative pt-2">
<Button
on:click={() =>
@@ -60,7 +60,7 @@
/>
{/key}
</div>
</svelte:fragment>
{/snippet}
</Tabs>
</div></DrawerContent
>
@@ -78,8 +78,8 @@
? 'bg-red-600 !border-blue-600'
: 'bg-red-400'
: selectedJobId == id
? 'text-blue-600'
: ''
? 'text-blue-600'
: ''
)}
on:click={() => {
selectedJobId = id
@@ -266,7 +266,7 @@
</div>
</Pane>
</Splitpanes>
<svelte:fragment slot="actions">
{#snippet actions()}
{#if refreshComponents}
<Button
size="md"
@@ -291,9 +291,9 @@
</Button>
{#if hasErrors}
<Button size="md" color="light" variant="border" on:click={() => dispatch('clearErrors')}>
>Clear Errors &nbsp;<BellOff size={14} />
Clear Errors &nbsp;<BellOff size={14} />
</Button>
{/if}
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
@@ -85,7 +85,7 @@
<JsonEditor bind:code />
{/if}
<svelte:fragment slot="actions">
{#snippet actions()}
{#if !$userStore?.operator}
<Button on:click={saveDraft} startIcon={{ icon: Save }} color="dark" size="xs">
Save as draft
@@ -94,6 +94,6 @@
Deploy
</Button>
{/if}
</svelte:fragment>
{/snippet}
</DrawerContent>
</Drawer>
@@ -24,47 +24,54 @@
import { CUSTOM_TAGS_SETTING, WORKSPACE_SLACK_BOT_TOKEN_PATH } from '$lib/consts'
import { loadSchemaFromPath } from '$lib/infer'
import { hubPaths } from '$lib/hub'
export let appPath: string
export let open = false
import { untrack } from 'svelte'
interface Props {
appPath: string
open?: boolean
}
let appReportingEnabled = false
let appReportingStartupDuration = 5
let { appPath, open = $bindable(false) }: Props = $props()
let appReportingEnabled = $state(false)
let appReportingStartupDuration = $state(5)
let appReportingSchedule: {
cron: string
timezone: string
} = {
} = $state({
cron: '0 0 12 * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
let selectedTab: 'email' | 'slack' | 'discord' | 'custom' = $enterpriseLicense
? 'slack'
: 'custom'
})
let selectedTab: 'email' | 'slack' | 'discord' | 'custom' = $state(
$enterpriseLicense ? 'slack' : 'custom'
)
let screenshotKind: 'pdf' | 'png' = 'pdf'
let screenshotKind: 'pdf' | 'png' = $state('pdf')
let customPath: string | undefined = undefined
let customPathSchema: Record<string, any> = {}
let args: Record<string, any> = {}
let areArgsValid = true
let customWidth: null | number = 1200
let customHeight: null | number = null
let customPath: string | undefined = $state(undefined)
let customPathSchema: Record<string, any> = $state({})
let args: Record<string, any> = $state({})
let areArgsValid = $state(true)
let customWidth: null | number = $state(1200)
let customHeight: null | number = $state(null)
$: customPath
? loadSchemaFromPath(customPath).then((schema) => {
customPathSchema = schema
? {
...schema,
properties: Object.fromEntries(
Object.entries(schema.properties ?? {}).filter(
([key, _]) => key !== 'screenshot' && key !== 'app_path' && key !== 'kind'
$effect(() => {
customPath
? loadSchemaFromPath(customPath).then((schema) => {
customPathSchema = schema
? {
...schema,
properties: Object.fromEntries(
Object.entries(schema.properties ?? {}).filter(
([key, _]) => key !== 'screenshot' && key !== 'app_path' && key !== 'kind'
)
)
)
}
: {}
})
: (customPathSchema = {})
}
: {}
})
: (customPathSchema = {})
})
let isSlackConnectedWorkspace = false
let isSlackConnectedWorkspace = $state(false)
async function getWorspaceSlackSetting() {
const settings = await WorkspaceService.getSettings({
workspace: $workspaceStore!
@@ -134,7 +141,9 @@
} catch (err) {}
}
$: appPath && getAppReportingInfo()
$effect(() => {
appPath && untrack(() => getAppReportingInfo())
})
async function disableAppReporting() {
const flowPath = appPath + '_reports'
@@ -496,7 +505,7 @@ export async function main(
appReportingEnabled = true
}
let testLoading = false
let testLoading = $state(false)
async function testReport() {
try {
testLoading = true
@@ -545,12 +554,14 @@ export async function main(
}
}
let disabled = true
$: disabled =
emptyString(appReportingSchedule.cron) ||
(selectedTab === 'custom' && emptyString(customPath)) ||
(selectedTab === 'slack' && !isSlackConnectedWorkspace) ||
!areArgsValid
let disabled = $state(true)
$effect(() => {
disabled =
emptyString(appReportingSchedule.cron) ||
(selectedTab === 'custom' && emptyString(customPath)) ||
(selectedTab === 'slack' && !isSlackConnectedWorkspace) ||
!areArgsValid
})
</script>
<DrawerContent
@@ -558,7 +569,8 @@ export async function main(
title="Schedule Reports"
tooltip="Send a PDF or PNG preview of any app at a given schedule"
documentationLink="https://www.windmill.dev/docs/apps/schedule_reports"
><svelte:fragment slot="actions">
>
{#snippet actions()}
<div class="mr-4 center-center">
<Toggle
checked={appReportingEnabled}
@@ -587,7 +599,7 @@ export async function main(
>
{appReportingEnabled ? 'Update' : 'Save and enable'}
</Button>
</svelte:fragment>
{/snippet}
<div class="flex flex-col gap-8">
<Alert type="info" title="Scheduled PDF/PNG reports"
>Send a PDF or PNG preview of the app at a given schedule. Enabling this feature will create a
@@ -6,7 +6,11 @@
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
export let loading: boolean = false
interface Props {
loading?: boolean
}
let { loading = false }: Props = $props()
const { mode, jobs, jobsById } = getContext<AppViewerContext>('AppViewerContext')
</script>
@@ -18,26 +22,27 @@
jobsById.set({})
jobs.set([])
}}
let:item
>
<ToggleButton
label="Editor"
value="dnd"
icon={Pen}
disabled={loading}
iconProps={{ size: 16 }}
class="gap-2"
{item}
/>
<ToggleButton
label="Preview"
value="preview"
icon={Eye}
tooltip="Preview mode"
disabled={loading}
id="app-editor-preview-toggle"
iconProps={{ size: 16 }}
class="gap-2"
{item}
/>
{#snippet children({ item })}
<ToggleButton
label="Editor"
value="dnd"
icon={Pen}
disabled={loading}
iconProps={{ size: 16 }}
class="gap-2"
{item}
/>
<ToggleButton
label="Preview"
value="preview"
icon={Eye}
tooltip="Preview mode"
disabled={loading}
id="app-editor-preview-toggle"
iconProps={{ size: 16 }}
class="gap-2"
{item}
/>
{/snippet}
</ToggleButtonGroup>

Some files were not shown because too many files have changed in this diff Show More