mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: better graph layout algorithm + migrate to svelte 5 almost everywhere + xyflow 1.0
This commit is contained in:
Generated
+26
-20
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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, ''')
|
||||
}
|
||||
|
||||
$: 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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' : ''}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 for 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 for 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 <BellOff size={14} />
|
||||
Clear Errors <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
Reference in New Issue
Block a user