allow more customisation of frontend (#4508)

* all

* npm check
This commit is contained in:
Ruben Fiszel
2024-10-08 19:34:54 -07:00
committed by GitHub
parent e44decb9c2
commit b579db61ec
59 changed files with 786 additions and 708 deletions
+22
View File
@@ -300,6 +300,14 @@
"types": "./package/utils.d.ts",
"default": "./package/utils.js"
},
"./icons/store": {
"types": "./package/icons/store.d.ts",
"default": "./package/icons/store.js"
},
"./script_helpers": {
"types": "./package/script_helpers.d.ts",
"default": "./package/script_helpers.js"
},
"./infer": {
"types": "./package/infer.d.ts",
"default": "./package/infer.js"
@@ -341,6 +349,11 @@
"types": "./package/components/DropdownV2.svelte.d.ts",
"svelte": "./package/components/DropdownV2.svelte",
"default": "./package/components/DropdownV2.svelte"
},
"./components/flows/FlowHistoryInner.svelte": {
"types": "./package/components/flows/FlowHistoryInner.svelte.d.ts",
"svelte": "./package/components/flows/FlowHistoryInner.svelte",
"default": "./package/components/flows/FlowHistoryInner.svelte"
}
},
"files": [
@@ -426,6 +439,9 @@
"components/EditableSchemaWrapper.svelte": [
"./package/components/schema/EditableSchemaWrapper.svelte.d.ts"
],
"components/flows/FlowHistoryInner.svelte": [
"./package/components/flows/FlowHistoryInner.svelte.d.ts"
],
"utils": [
"./package/utils.d.ts"
],
@@ -461,6 +477,12 @@
],
"components/DropdownV2.svelte": [
"./package/components/DropdownV2.svelte.d.ts"
],
"script_helpers": [
"./package/script_helpers.d.ts"
],
"icons/store": [
"./package/icons/store.d.ts"
]
}
},
@@ -1,5 +1,4 @@
<script lang="ts">
import { page } from '$app/stores'
import { base } from '$app/paths'
import AppConnectInner from '$lib/components/AppConnectInner.svelte'
@@ -8,7 +7,10 @@
import { workspaceStore } from '$lib/stores'
import { onMount } from 'svelte'
let resourceType = $page.url.searchParams.get('resource_type') ?? undefined
export let resourceType: string | undefined = undefined
export let workspace: string
export let express = false
let step = 1
let disabled = false
let isGoogleSignin = false
@@ -17,8 +19,6 @@
let appConnect: AppConnectInner | undefined = undefined
let darkMode: boolean = false
const workspace = $page.url.searchParams.get('workspace')
const express = $page.url.searchParams.get('express') == 'true'
if (workspace) {
$workspaceStore = workspace
@@ -68,11 +68,7 @@
bind:isGoogleSignin
bind:disabled
bind:manual
on:error={(e) => {
window?.parent?.postMessage({ type: 'error', error: e.detail }, '*')
}}
on:refresh={(e) => {
window?.parent?.postMessage({ type: 'refresh', detail: e.detail }, '*')
}}
on:error
on:refresh
/>
</div>
+6 -2
View File
@@ -89,6 +89,7 @@
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 }
@@ -537,11 +538,14 @@
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format.split('-').length > 1 && resourceTypes.includes(format.substring('resource-'.length))))}
<ObjectResourceInput
{defaultValue}
selectFirst
selectFirst={!noDefaultOnSelectFirst}
{disablePortal}
{format}
bind:value
bind:editor
on:clear={() => {
defaultValue = null
}}
{showSchemaExplorer}
/>
{:else if inputCat == 'resource-object' && format.split('-').length > 1 && format
@@ -807,7 +811,7 @@
</div>
{:else if inputCat == 'resource-string'}
<ResourcePicker
selectFirst
selectFirst={noDefaultOnSelectFirst}
{disablePortal}
bind:value
initialValue={defaultValue}
@@ -67,7 +67,7 @@
</button>
{/if}
{#if showTooltip && !disablePopup}
<Portal>
<Portal name="custom-popover">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
use:popperContent={popperOptions}
@@ -854,7 +854,7 @@
</DrawerContent>
</Drawer>
<Portal>
<Portal name="s3filepicker">
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
</Portal>
{/if}
@@ -99,6 +99,7 @@
export let diffDrawer: DiffDrawer | undefined = undefined
export let customUi: FlowBuilderWhitelabelCustomUi = {}
export let disableAi: boolean = false
export let disabledFlowInputs = false
$: setContext('customUi', customUi)
@@ -1326,6 +1327,7 @@
<!-- metadata -->
{#if $flowStateStore}
<FlowEditor
{disabledFlowInputs}
disableAi={disableAi || customUi?.stepInputs?.ai == false}
disableSettings={customUi?.settingsPanel === false}
{loading}
@@ -0,0 +1,29 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import FieldHeader from './FieldHeader.svelte'
export let schema: Schema | { [key: string]: unknown } | undefined
</script>
<ul class="my-2">
{#if schema == undefined}
<li class="text-secondary text-xs italic mb-4">No inputs</li>
{:else}
{#each Object.entries(schema.properties ?? {}) as [inp, v]}
<li class="list-disc flex flex-row items-center">
<FieldHeader
label={inp}
required={Array.isArray(schema.required) && schema.required?.includes(inp)}
type={v?.type}
contentEncoding={v?.contentEncoding}
format={v?.format}
/><span class="ml-4 mt-1 text-xs"
>{v?.default != undefined && v?.default != ''
? 'default: ' + JSON.stringify(v?.default)
: ''}</span
>
</li>
{/each}
{/if}
</ul>
@@ -26,7 +26,7 @@
{#if job['success'] != undefined}
Received job: {displayDate(job.created_at ?? '')}
{:else}
Received job <TimeAgo withDate date={job.created_at ?? ''} />
Received job <TimeAgo date={job.created_at ?? ''} />
{/if}
<Tooltip small>{job?.created_at}</Tooltip>
</span>
@@ -35,7 +35,7 @@
<div class="flex flex-row gap-2 items-center text-sm">
<Clock size={SMALL_ICON_SIZE} class="text-secondary min-w-3.5" />
<span class="whitespace-nowrap">
Started <TimeAgo withDate agoOnlyIfRecent date={job.started_at ?? ''} />
Started <TimeAgo agoOnlyIfRecent date={job.started_at ?? ''} />
<Tooltip small>{job?.started_at}</Tooltip>
</span>
</div>
+2 -22
View File
@@ -2,11 +2,11 @@
import type { FlowValue } from '$lib/gen'
import { Tab, Tabs, TabContent } from './common'
import SchemaViewer from './SchemaViewer.svelte'
import FieldHeader from './FieldHeader.svelte'
import FlowGraphViewer from './FlowGraphViewer.svelte'
import HighlightTheme from './HighlightTheme.svelte'
import FlowViewerInner from './FlowViewerInner.svelte'
import FlowInputViewer from './FlowInputViewer.svelte'
export let flow: {
summary: string
@@ -27,10 +27,6 @@
if (initialOpen) {
open[initialOpen] = true
}
function toAny(x: unknown): any {
return x as any
}
</script>
<HighlightTheme />
@@ -54,23 +50,7 @@
<span>Flow Input</span>
</p>
{#if flow.schema && flow.schema.properties && Object.keys(flow.schema.properties).length > 0 && flow.schema}
<ul class="my-2">
{#each Object.entries(flow.schema.properties) as [inp, v]}
<li class="list-disc flex flex-row">
<FieldHeader
label={inp}
required={flow.schema.required?.includes(inp)}
type={toAny(v)?.type}
contentEncoding={toAny(v)?.contentEncoding}
format={toAny(v)?.format}
/><span class="ml-4 mt-2 text-xs"
>{toAny(v)?.default != undefined
? 'default: ' + JSON.stringify(toAny(v)?.default)
: ''}</span
>
</li>
{/each}
</ul>
<FlowInputViewer schema={flow.schema} />
{:else}
<div class="text-secondary text-xs italic mb-4">No inputs</div>
{/if}
@@ -1,6 +1,5 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import { base } from '$lib/base'
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher, getContext } from 'svelte'
import Select from './apps/svelte-select/lib/index'
@@ -8,7 +7,7 @@
import DarkModeObserver from './DarkModeObserver.svelte'
import { Button, Drawer, DrawerContent } from './common'
import { Plus } from 'lucide-svelte'
import { Plus, Loader2 } from 'lucide-svelte'
import type { AppViewerContext } from './apps/types'
import { sendUserToast } from '$lib/toast'
@@ -65,23 +64,6 @@
refreshCount += 1
open = true
}
function processEvent(event: MessageEvent) {
if (event.origin !== window.location.origin) {
return
}
if (event.data.type === 'error') {
sendUserToast(event.data.error, true)
}
if (event.data.type === 'refresh') {
window.removeEventListener('message', processEvent)
value = event.data.detail
valueSelect = { value, label: value }
drawer?.closeDrawer?.()
open = false
}
}
</script>
<DarkModeObserver bind:darkMode />
@@ -89,12 +71,15 @@
{#if expressOAuthSetup}
{#if open}
{#key refreshCount}
<iframe
title="App connection"
class="w-full h-full hidden"
src="{base}/embed_connect?resource_type={resourceType}&workspace={appViewerContext?.workspace ??
$workspaceStore}&express=true"
/>
{#await import('./AppConnectLightweightResourcePicker.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
workspace={appViewerContext?.workspace ?? $workspaceStore}
{resourceType}
express={true}
/>
{/await}
{/key}
{/if}
{:else}
@@ -105,12 +90,31 @@
tooltip="Resources represent connections to third party systems. Learn more on how to integrate external APIs."
documentationLink="https://www.windmill.dev/docs/integrations/integrations_on_windmill"
>
<iframe
{#await import('./AppConnectLightweightResourcePicker.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
workspace={appViewerContext?.workspace ?? $workspaceStore}
{resourceType}
express={false}
on:error={(e) => {
sendUserToast(e.detail, true)
}}
on:refresh={(e) => {
value = e.detail
valueSelect = { value, label: value }
drawer?.closeDrawer?.()
open = false
}}
/>
{/await}
<!-- <iframe
title="App connection"
class="w-full h-full"
src="{base}/embed_connect?resource_type={resourceType}&workspace={appViewerContext?.workspace ??
$workspaceStore}&express=false"
/>
/> -->
</DrawerContent>
</Drawer>
{/if}
@@ -142,13 +146,11 @@
<Button
{disabled}
color="light"
variant="border"
size="xs"
variant="contained"
btnClasses="w-8 px-0.5 py-1.5"
size="sm"
on:click={() => {
open = true
window.removeEventListener('message', processEvent)
window.addEventListener('message', processEvent)
drawer?.openDrawer?.()
}}
startIcon={{ icon: Plus }}
@@ -27,7 +27,7 @@
<slot />
</fragment>
{#if showTooltip}
<Portal>
<Portal name="manual-popover">
<div
use:popperContent
class={twMerge('z-[901] rounded-lg shadow-md border p-4 bg-surface', $$props.class)}
@@ -11,7 +11,6 @@
export let selectFirst = false
export let defaultValue: any
export let editor: SimpleEditor | undefined = undefined
function isString(value: any) {
return typeof value === 'string' || value instanceof String
}
@@ -19,7 +18,7 @@
export let path: string = ''
function resourceToValue() {
if (path) {
if (path && path != '') {
value = `$res:${path}`
} else {
value = undefined
@@ -50,8 +49,11 @@
path = e.detail
resourceToValue()
}}
on:clear
bind:value={path}
initialValue={defaultValue}
initialValue={typeof defaultValue == 'string' && defaultValue.startsWith('$res:')
? defaultValue.substr('$res:'.length)
: defaultValue}
resourceType={format.split('-').length > 1 ? format.substring('resource-'.length) : undefined}
{showSchemaExplorer}
/>
+1 -1
View File
@@ -67,7 +67,7 @@
</button>
{/if}
{#if showTooltip && !disablePopup}
<Portal>
<Portal name="popover">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
use:popperContent={popperOptions}
+7 -8
View File
@@ -1,13 +1,8 @@
<script context="module">
import { tick } from 'svelte'
/**
* Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}>
*
* @param {HTMLElement} el
* @param {HTMLElement|string} target DOM Element or CSS Selector
*/
export function portal(el, target = 'body') {
export function portal(el, options) {
let { target, name } = options
let targetEl
async function update(newTarget) {
target = newTarget
@@ -32,6 +27,9 @@
if (!el.classList.contains('windmill-app')) {
el.classList.add('windmill-app')
}
if (name && !el.classList.contains(name)) {
el.classList.add(name)
}
targetEl.appendChild(el)
el.hidden = false
}
@@ -56,8 +54,9 @@
* @type { HTMLElement|string}
*/
export let target = 'body'
export let name = undefined
</script>
<div use:portal={target} hidden>
<div use:portal={{ target, name }} hidden>
<slot />
</div>
@@ -86,7 +86,7 @@
sendUserToast(`Updated resource at ${path}`)
dispatch('refresh', path)
} else {
throw Error('Cannot edit undefined resourceToEdit')
throw Error('Cannot edit undefined resource')
}
}
@@ -231,11 +231,10 @@
<Skeleton layout={[[4]]} />
{:else if !viewJsonSchema && resourceSchema && resourceSchema?.properties}
{#if resourceTypeInfo?.format_extension}
<h5 class="mt-4 inline-flex items-center gap-4 pb-2">
File content ({resourceTypeInfo.format_extension})
</h5>
<h5 class="mt-4 inline-flex items-center gap-4 pb-2">
File content ({resourceTypeInfo.format_extension})
</h5>
<div class="h-full w-full border p-1 rounded">
<SimpleEditor
autoHeight
class="editor"
@@ -116,7 +116,7 @@
/>
<div class="flex flex-col w-full items-start">
<div class="flex flex-row gap-x-1 w-full items-center">
<div class="flex flex-row w-full items-center">
{#if collection?.length > 0}
<Select
{disabled}
@@ -128,9 +128,11 @@
valueSelect = e.detail
}}
on:clear={() => {
initialValue = undefined
value = undefined
valueType = undefined
valueSelect = undefined
dispatch('clear')
}}
items={collection}
class="text-clip grow min-w-0"
@@ -143,14 +145,15 @@
{:else if !loading}
<div class="text-2xs text-tertiary mr-2">0 found</div>
{/if}
{#if !loading}
<div class="mx-0.5" />
{#if value && value != ''}
<Button
{disabled}
color="light"
variant="border"
size="xs"
variant="contained"
size="sm"
btnClasses="w-8 px-0.5 py-1.5"
on:click={() => resourceEditor?.initEdit?.(value ?? '')}
startIcon={{ icon: Pen }}
iconOnly
@@ -162,8 +165,9 @@
<Button
{disabled}
color="light"
variant="border"
size="xs"
variant="contained"
size="sm"
btnClasses="w-8 px-0.5 py-1.5"
on:click={() => appConnect?.open?.(rt)}
startIcon={{ icon: Plus }}>{rt}</Button
>
@@ -172,8 +176,9 @@
<Button
{disabled}
color="light"
variant="border"
size="xs"
variant="contained"
size="sm"
btnClasses="w-8 px-0.5 py-1.5"
on:click={() => appConnect?.open?.(resourceType, expressOAuthSetup)}
startIcon={{ icon: Plus }}
iconOnly={collection?.length > 0}
@@ -185,9 +190,10 @@
{/if}
<Button
variant="border"
variant="contained"
color="light"
size="xs"
btnClasses="w-8 px-0.5 py-1.5"
size="sm"
on:click={() => {
loadResources(resourceType)
}}
+1 -1
View File
@@ -107,7 +107,7 @@
<div class="flex items-center gap-2">
<span class="text-sm text-tertiary">
{#if runnable}
Edited <TimeAgo withDate agoOnlyIfRecent date={runnable.created_at || ''} /> by {runnable.created_by ||
Edited <TimeAgo agoOnlyIfRecent date={runnable.created_at || ''} /> by {runnable.created_by ||
'unknown'}
{/if}
</span>
@@ -364,7 +364,7 @@
<div
class="whitespace-nowrap col-span-3 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
<TimeAgo date={i.started_at ?? ''} />
<TimeAgo noDate date={i.started_at ?? ''} />
</div>
<div class="col-span-2">
<a
@@ -425,7 +425,7 @@
<div
class="whitespace-nowrap col-span-2 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
>
<TimeAgo date={i.created_at ?? ''} />
<TimeAgo noDate date={i.created_at ?? ''} />
</div>
<div class="col-span-1">
<a
@@ -1,6 +1,5 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import PanelSection from './apps/editor/settingsPanel/common/PanelSection.svelte'
import { classNames, emptyString } from '$lib/utils'
import { ScriptService, type ScriptHistory } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
@@ -63,70 +62,66 @@
<Splitpanes class="!overflow-visible">
<Pane size={20}>
<PanelSection title="Past Versions">
<div class="flex flex-col gap-2 w-full">
{#if !loading}
{#if versions && versions.length > 0}
<div class="flex gap-2 flex-col">
{#each versions ?? [] as version, versionIndex}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer ',
selectedVersion?.script_hash == version.script_hash
? 'bg-surface-selected'
: '',
'hover:bg-surface-hover'
)}
on:click={() => {
selectedVersion = version
selectedVersionIndex = versionIndex
<div class="flex flex-col gap-2 px-2 pt-2 w-full">
{#if !loading}
{#if versions && versions.length > 0}
<div class="flex gap-2 flex-col">
{#each versions ?? [] as version, versionIndex}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer ',
selectedVersion?.script_hash == version.script_hash ? 'bg-surface-selected' : '',
'hover:bg-surface-hover'
)}
on:click={() => {
selectedVersion = version
selectedVersionIndex = versionIndex
if (showDiff && versions && selectedVersionIndex === versions.length - 1) {
showDiff = false
}
if (showDiff && versions && selectedVersionIndex === versions.length - 1) {
showDiff = false
}
const availableVersions = versions?.slice(selectedVersionIndex + 1)
const availableVersions = versions?.slice(selectedVersionIndex + 1)
if (
previousHash &&
!availableVersions?.find((v) => v.script_hash === previousHash)
) {
previousHash = availableVersions?.[0]?.script_hash
}
if (
previousHash &&
!availableVersions?.find((v) => v.script_hash === previousHash)
) {
previousHash = availableVersions?.[0]?.script_hash
}
deploymentMsgUpdate = undefined
deploymentMsgUpdateMode = false
}}
>
<span class="text-xs truncate">
{#if emptyString(version.deployment_msg)}Version {version.script_hash}{:else}{version.deployment_msg}{/if}
</span>
{#if openDetails}
<Button
on:click={() => {
dispatch('openDetails', { version: version.script_hash })
}}
class="ml-2 inline-flex gap-1 text-xs items-center"
size="xs"
color="light"
variant="border"
>
Run page<ExternalLink size={14} />
</Button>
{/if}
</div>
{/each}
</div>
{:else}
<div class="text-sm text-tertiary">No items</div>
{/if}
deploymentMsgUpdate = undefined
deploymentMsgUpdateMode = false
}}
>
<span class="text-xs truncate">
{#if emptyString(version.deployment_msg)}Version {version.script_hash}{:else}{version.deployment_msg}{/if}
</span>
{#if openDetails}
<Button
on:click={() => {
dispatch('openDetails', { version: version.script_hash })
}}
class="ml-2 inline-flex gap-1 text-xs items-center"
size="xs"
color="light"
variant="border"
>
Run page<ExternalLink size={14} />
</Button>
{/if}
</div>
{/each}
</div>
{:else}
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
<div class="text-sm text-tertiary">No items</div>
{/if}
</div>
</PanelSection>
{:else}
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
{/if}
</div>
</Pane>
<Pane size={80}>
<div class="h-full w-full overflow-auto">
+5 -6
View File
@@ -4,8 +4,8 @@
import { onDestroy, onMount } from 'svelte'
export let date: string
export let withDate: boolean = false
export let agoOnlyIfRecent: boolean = false
export let noDate = false
let computedTimeAgo: string | undefined = undefined
@@ -63,21 +63,20 @@
let dAgo = daysAgo(date)
if (dAgo == 0) {
return `yesterday at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
} else if (dAgo > 7 && withDate) {
} else if (dAgo > 7 && !noDate) {
return `${dAgo + 1} days ago at ${date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}`
} else {
return !withDate ? displayDate(dateString, false, withDate) : ''
return displayDate(dateString, false, !noDate)
}
}
}
</script>
{#if withDate}
{displayDate(date)}
{/if}
{#if computedTimeAgo && (!agoOnlyIfRecent || isRecent)}
{computedTimeAgo}
{:else}
{displayDate(date)}
{/if}
@@ -244,7 +244,7 @@
</RunnableWrapper>
{#if resolvedConfig?.confirmationModal?.selected === 'confirmationModal'}
<Portal target="#app-editor-top-level-drawer">
<Portal name="app-button" target="#app-editor-top-level-drawer">
<ConfirmationModal
open={Boolean(confirmedCallback)}
title={resolvedConfig?.confirmationModal?.configuration?.confirmationModal?.title ?? ''}
@@ -695,7 +695,7 @@
{/if}
</div>
</RunnableWrapper>
<Portal>
<Portal name="db-explorer">
<Drawer bind:this={insertDrawer} size="800px">
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer}>
<svelte:fragment slot="actions">
@@ -186,7 +186,7 @@
{option}
</div>
</MultiSelect>
<Portal>
<Portal name="app-multiselect">
<div use:floatingContent class="z5000" hidden={!open}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -184,7 +184,7 @@
{option}
</div>
</MultiSelect>
<Portal>
<Portal name="app-multiselect-v2">
<div use:floatingContent class="z5000" hidden={!open}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -113,7 +113,7 @@
</AlignWrapper>
</div>
<Portal target="#app-editor-top-level-drawer">
<Portal target="#app-editor-top-level-drawer" name="app-drawer">
<Drawer
let:open
bind:this={appDrawer}
@@ -145,7 +145,7 @@
</AlignWrapper>
</div>
{/if}
<Portal target="#app-editor-top-level-drawer">
<Portal target="#app-editor-top-level-drawer" name="app-modal">
<Disposable
{id}
let:handleClickAway
@@ -167,7 +167,7 @@
<slot />
{#if contextMenuVisible}
<Portal>
<Portal name="grid-editor">
<div style="position: fixed; top: {menuY}px; left: {menuX}px; z-index:6000;">
<div class="rounded-md bg-surface border shadow-md divide-y w-64">
<div class="p-1" use:clickOutside={false}>
@@ -12,7 +12,7 @@
</script>
{#if condition}
<Portal {target}><slot /></Portal>
<Portal name="conditional-portal-select" {target}><slot /></Portal>
{:else}
<slot />
{/if}
@@ -5,7 +5,7 @@
</script>
{#if condition}
<Portal><slot /></Portal>
<Portal name="conditional-portal-global"><slot /></Portal>
{:else}
<slot />
{/if}
@@ -41,7 +41,7 @@
</MenuButton>
</span>
<Portal {target}>
<Portal name="button-dropdown" {target}>
<div use:popperContent={popperOptions} class="z-[6000]">
<Transition
show={open}
@@ -6,7 +6,7 @@
</script>
{#if condition}
<Portal {target}><slot /></Portal>
<Portal name="conditional-portal" {target}><slot /></Portal>
{:else}
<slot />
{/if}
@@ -25,7 +25,7 @@
<slot name="trigger" />
</MenuButton>
</div>
<Portal>
<Portal name="menu-v2">
<div use:floatingContent class="z-[6000]">
<Transition
{open}
@@ -24,7 +24,7 @@
}
</script>
<Portal target="#app-editor-top-level-drawer">
<Portal name="always-mounted" target="#app-editor-top-level-drawer">
<div
class={twMerge(
`${
@@ -40,7 +40,7 @@
<slot {pointerup} {pointerdown} name="button" />
</div>
<Portal {target}>
<Portal name="popup-v2" {target}>
{#if open}
<div
class="border rounded-lg shadow-lg bg-surface z5000"
@@ -38,7 +38,7 @@
</MenuButton>
</span>
<Portal>
<Portal name="menu">
<div use:popperContent={popperOptions} class="z-[2000]">
<Transition
show={open}
@@ -16,6 +16,7 @@
export let disableTutorials = false
export let disableAi = false
export let disableSettings = false
export let disabledFlowInputs = false
export let smallErrorHandler = false
let size = 50
@@ -61,7 +62,7 @@
</div>
</div>
{:else}
<FlowEditorPanel enableAi={!disableAi} />
<FlowEditorPanel {disabledFlowInputs} enableAi={!disableAi} />
{/if}
</Pane>
</Splitpanes>
@@ -1,88 +1,18 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import PanelSection from '../apps/editor/settingsPanel/common/PanelSection.svelte'
import { classNames, displayDate, emptyString, sendUserToast } from '$lib/utils'
import { type Flow, FlowService, type FlowVersion } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Skeleton } from '$lib/components/common'
import { createEventDispatcher } from 'svelte'
import Drawer from '../common/drawer/Drawer.svelte'
import DrawerContent from '../common/drawer/DrawerContent.svelte'
import Button from '../common/button/Button.svelte'
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import FlowHistoryInner from './FlowHistoryInner.svelte'
export let path: string
let drawer: Drawer
let loading: boolean = false
let versions: FlowVersion[] = []
let selectedVersion: FlowVersion | undefined = undefined
let selected: Flow | undefined = undefined
let deploymentMsgUpdateMode = false
let deploymentMsgUpdate: string | undefined = undefined
export function open() {
loadVersions()
drawer.openDrawer()
}
async function loadFlow(version: number) {
selected = await FlowService.getFlowVersion({
workspace: $workspaceStore!,
version,
path
})
}
async function loadVersions() {
loading = true
versions = await FlowService.getFlowHistory({
workspace: $workspaceStore!,
path: path
})
loading = false
}
async function updateDeploymentMsg(version: number | undefined) {
if (
selectedVersion === undefined ||
version === undefined ||
emptyString(deploymentMsgUpdate)
) {
return
}
await FlowService.updateFlowHistory({
workspace: $workspaceStore!,
version,
path,
requestBody: {
deployment_msg: deploymentMsgUpdate!
}
})
selectedVersion.deployment_msg = deploymentMsgUpdate
deploymentMsgUpdateMode = false
loadVersions()
}
const dispatch = createEventDispatcher()
async function restoreVersion(flow: Flow | undefined) {
if (!flow) return
await FlowService.updateFlow({
workspace: $workspaceStore!,
requestBody: {
...flow,
path
},
path
})
dispatch('historyRestore')
drawer?.closeDrawer()
sendUserToast('Flow restored from previous deployment')
}
$: selectedVersion !== undefined && loadFlow(selectedVersion.id)
</script>
<Drawer bind:this={drawer} size="1200px">
@@ -91,136 +21,14 @@
drawer?.closeDrawer()
}}
noPadding
title="Deployment History"
>
<Splitpanes class="!overflow-visible">
<Pane size={20}>
<PanelSection title="Past Deployments">
<div class="flex flex-col gap-2 w-full">
{#if !loading}
{#if versions.length > 0}
<div class="flex gap-2 flex-col">
{#each versions ?? [] as version}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
selectedVersion?.id == version.id ? 'bg-blue-100 text-blue-600' : ''
)}
role="button"
tabindex="0"
on:click={() => {
selectedVersion = version
}}
>
<span class="text-xs truncate">
{#if emptyString(version.deployment_msg)}Version {version.id}{:else}{version.deployment_msg}{/if}
</span>
</div>
{/each}
</div>
{:else}
<div class="text-sm text-tertiary">No items</div>
{/if}
{:else}
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
{/if}
</div>
</PanelSection>
</Pane>
<Pane size={80}>
<div class="h-full w-full overflow-auto">
{#if selectedVersion}
{#if selected}
<div class="px-2 flex flex-col gap-2">
<span class="flex flex-row text-sm p-1 text-tertiary">
{#if deploymentMsgUpdateMode}
<div class="flex w-full">
<input
type="text"
bind:value={deploymentMsgUpdate}
class="!w-auto grow"
on:click|stopPropagation={() => {}}
on:keydown|stopPropagation
on:keypress|stopPropagation={({ key }) => {
if (key === 'Enter') updateDeploymentMsg(selectedVersion?.id)
}}
/>
<Button
size="xs"
color="blue"
buttonType="button"
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Save Deployment Message"
on:click={() => {
updateDeploymentMsg(selectedVersion?.id)
}}
>
<ArrowRight size={14} />
</Button>
<Button
size="xs"
color="light"
buttonType="button"
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Abort"
on:click={() => {
deploymentMsgUpdateMode = false
deploymentMsgUpdate = undefined
}}
>
<X size={14} />
</Button>
</div>
{:else}
{#if selectedVersion.deployment_msg}
{selectedVersion.deployment_msg}
{:else}
Deployed {displayDate(selected.edited_at)} by {selected.edited_by}
{/if}
<button
on:click={() => {
deploymentMsgUpdate = selectedVersion?.deployment_msg
deploymentMsgUpdateMode = true
}}
title="Update commit message"
class="flex items-center px-1 rounded-sm hover:text-primary text-secondary h-5"
aria-label="Update commit message"
>
<Pencil size={14} />
</button>
{/if}
</span>
<div class="flex p-1 gap-2">
<Button
size="xs"
on:click={() =>
window.open(
`/flows/add?template_id=${selectedVersion?.id}&template=${path}`,
'_blank'
)}
>
Restore as fork
</Button>
<Button size="xs" on:click={() => restoreVersion(selected)}
>Redeploy with that version
</Button>
</div>
{#await import('$lib/components/FlowViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default flow={selected} />
{/await}
</div>
{:else}
<Skeleton layout={[[40]]} />
{/if}
{:else}
<div class="text-sm p-2 text-tertiary"
>Select a deployment version to see its details</div
>
{/if}
</div>
</Pane>
</Splitpanes>
<FlowHistoryInner
on:historyRestore={() => {
drawer.closeDrawer()
dispatch('historyRestore')
}}
{path}
/>
</DrawerContent>
</Drawer>
@@ -0,0 +1,212 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { classNames, displayDate, emptyString, sendUserToast } from '$lib/utils'
import { type Flow, FlowService, type FlowVersion } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Skeleton } from '$lib/components/common'
import Button from '../common/button/Button.svelte'
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
export let path: string
let loading: boolean = false
let versions: FlowVersion[] = []
let selectedVersion: FlowVersion | undefined = undefined
let selected: Flow | undefined = undefined
let deploymentMsgUpdateMode = false
let deploymentMsgUpdate: string | undefined = undefined
async function loadFlow(version: number) {
selected = await FlowService.getFlowVersion({
workspace: $workspaceStore!,
version,
path
})
}
async function loadVersions() {
loading = true
versions = await FlowService.getFlowHistory({
workspace: $workspaceStore!,
path: path
})
loading = false
}
async function updateDeploymentMsg(version: number | undefined) {
if (
selectedVersion === undefined ||
version === undefined ||
emptyString(deploymentMsgUpdate)
) {
return
}
await FlowService.updateFlowHistory({
workspace: $workspaceStore!,
version,
path,
requestBody: {
deployment_msg: deploymentMsgUpdate!
}
})
selectedVersion.deployment_msg = deploymentMsgUpdate
deploymentMsgUpdateMode = false
loadVersions()
}
const dispatch = createEventDispatcher()
async function restoreVersion(flow: Flow | undefined) {
if (!flow) return
await FlowService.updateFlow({
workspace: $workspaceStore!,
requestBody: {
...flow,
path
},
path
})
dispatch('historyRestore')
sendUserToast('Flow restored from previous deployment')
}
loadVersions()
$: selectedVersion !== undefined && loadFlow(selectedVersion.id)
</script>
<Splitpanes class="!overflow-visible">
<Pane size={20}>
<div class="flex flex-col gap-2 w-full px-2 py-2">
{#if !loading}
{#if versions.length > 0}
<div class="flex gap-2 flex-col">
{#each versions ?? [] as version}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-surface-hover hover:text-primary',
selectedVersion?.id == version.id ? 'bg-surface-selected text-primary' : ''
)}
role="button"
tabindex="0"
on:click={() => {
selectedVersion = version
}}
>
<span class="text-xs truncate">
{#if emptyString(version.deployment_msg)}Version {version.id}{:else}{version.deployment_msg}{/if}
</span>
</div>
{/each}
</div>
{:else}
<div class="text-sm text-tertiary">No items</div>
{/if}
{:else}
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
{/if}
</div>
</Pane>
<Pane size={80}>
<div class="h-full w-full overflow-auto pt-2">
{#if selectedVersion}
{#if selected}
<div class="px-2 flex flex-col gap-2">
<div class="flex justify-between">
<span class="flex flex-row text-sm p-1 text-tertiary">
{#if deploymentMsgUpdateMode}
<div class="flex w-full">
<input
type="text"
bind:value={deploymentMsgUpdate}
class="!w-auto grow"
on:click|stopPropagation={() => {}}
on:keydown|stopPropagation
on:keypress|stopPropagation={({ key }) => {
if (key === 'Enter') updateDeploymentMsg(selectedVersion?.id)
}}
/>
<Button
size="xs"
color="blue"
buttonType="button"
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Save Deployment Message"
on:click={() => {
updateDeploymentMsg(selectedVersion?.id)
}}
>
<ArrowRight size={14} />
</Button>
<Button
size="xs"
color="light"
buttonType="button"
btnClasses="!p-1 !w-[34px] !ml-1"
aria-label="Abort"
on:click={() => {
deploymentMsgUpdateMode = false
deploymentMsgUpdate = undefined
}}
>
<X size={14} />
</Button>
</div>
{:else}
{#if selectedVersion.deployment_msg}
{selectedVersion.deployment_msg}
{:else}
Deployed {displayDate(selected.edited_at)} by {selected.edited_by}
{/if}
<button
on:click={() => {
deploymentMsgUpdate = selectedVersion?.deployment_msg
deploymentMsgUpdateMode = true
}}
title="Update commit message"
class="flex items-center px-1 rounded-sm hover:text-primary text-secondary h-5"
aria-label="Update commit message"
>
<Pencil size={14} />
</button>
{/if}
</span>
<div class="flex p-1 gap-2">
<div class="flex">
<Button
size="sm"
color="dark"
on:click={() =>
window.open(
`/flows/add?template_id=${selectedVersion?.id}&template=${path}`,
'_blank'
)}
>
Restore as fork
</Button>
</div>
<div class="flex">
<Button size="sm" color="dark" on:click={() => restoreVersion(selected)}
>Redeploy with that version
</Button>
</div>
</div>
</div>
{#await import('$lib/components/FlowViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default flow={selected} />
{/await}
</div>
{:else}
<Skeleton layout={[[40]]} />
{/if}
{:else}
<div class="text-sm p-2 text-tertiary">Select a deployment version to see its details</div>
{/if}
</div>
</Pane>
</Splitpanes>
@@ -14,6 +14,7 @@
export let noEditor = false
export let enableAi = false
export let disabledFlowInputs = false
const { selectedId, flowStore, flowStateStore, flowInputsStore } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -57,7 +58,7 @@
{#if $selectedId?.startsWith('settings')}
<FlowSettings {noEditor} />
{:else if $selectedId === 'Input'}
<FlowInput {noEditor} />
<FlowInput {noEditor} disabled={disabledFlowInputs} />
{:else if $selectedId === 'Result'}
<p class="p-4 text-secondary">Nothing to show about the result node. Happy flow building!</p>
{:else if $selectedId === 'constants'}
@@ -12,8 +12,10 @@
import SavedInputs from '$lib/components/SavedInputs.svelte'
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
import AddProperty from '$lib/components/schema/AddProperty.svelte'
import FlowInputViewer from '$lib/components/FlowInputViewer.svelte'
export let noEditor: boolean
export let disabled: boolean
const { flowStore, flowStateStore, previewArgs, initialPath } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -42,67 +44,73 @@
<CapturePayload bind:this={capturePayload} />
<FlowCard {noEditor} title="Flow Input">
<div class="flex flex-row items-center gap-2 px-4 py-2 border-b">
<div>Copy input's schema from</div>
<Button
color="dark"
size="xs"
on:click={() => {
capturePayload.openDrawer()
}}
>
A request
</Button>
<Button
color="dark"
size="xs"
on:click={() => {
jsonPayload.openDrawer()
}}
>
A JSON
</Button>
<Button
color="dark"
size="xs"
on:click={() => {
inputLibraryDrawer.openDrawer()
}}
>
Past Runs/Input library
</Button>
<Button
color="dark"
size="xs"
disabled={$flowStore.value.modules.length === 0 ||
$flowStore.value.modules[0].value.type == 'identity'}
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
>
First step's inputs
</Button>
</div>
<div class="p-4 border-b">
<AddProperty
bind:schema={$flowStore.schema}
bind:this={addProperty}
on:change={() => {
$flowStore = $flowStore
}}
/>
</div>
{#if !disabled}
<div class="flex flex-row items-center gap-2 px-4 py-2 border-b">
<div>Copy input's schema from</div>
<Button
color="dark"
size="xs"
on:click={() => {
capturePayload.openDrawer()
}}
>
A request
</Button>
<Button
color="dark"
size="xs"
on:click={() => {
jsonPayload.openDrawer()
}}
>
A JSON
</Button>
<Button
color="dark"
size="xs"
on:click={() => {
inputLibraryDrawer.openDrawer()
}}
>
Past Runs/Input library
</Button>
<Button
color="dark"
size="xs"
disabled={$flowStore.value.modules.length === 0 ||
$flowStore.value.modules[0].value.type == 'identity'}
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
>
First step's inputs
</Button>
</div>
<div class="p-4 border-b">
<AddProperty
bind:schema={$flowStore.schema}
bind:this={addProperty}
on:change={() => {
$flowStore = $flowStore
}}
/>
</div>
<EditableSchemaForm
bind:schema={$flowStore.schema}
isFlowInput
on:edit={(e) => {
addProperty?.openDrawer(e.detail)
}}
on:delete={(e) => {
addProperty?.handleDeleteArgument([e.detail])
}}
offset={yOffset}
displayWebhookWarning
/>
<EditableSchemaForm
bind:schema={$flowStore.schema}
isFlowInput
on:edit={(e) => {
addProperty?.openDrawer(e.detail)
}}
on:delete={(e) => {
addProperty?.handleDeleteArgument([e.detail])
}}
offset={yOffset}
displayWebhookWarning
/>
{:else}
<div class="p-4 border-b">
<FlowInputViewer schema={$flowStore.schema} />
</div>
{/if}
</FlowCard>
<Drawer bind:this={jsonPayload} size="800px">
@@ -61,7 +61,7 @@
<div class="flex flex-col flex-1 h-full overflow-auto p-2">
{#if showDate && date}
<span class="text-xs text-tertiary mb-4"><TimeAgo agoOnlyIfRecent withDate {date} /></span>
<span class="text-xs text-tertiary mb-4"><TimeAgo agoOnlyIfRecent {date} /></span>
{/if}
{#if notFound}
<div class="text-red-400">script not found at {path} in workspace {$workspaceStore}</div>
@@ -256,7 +256,7 @@
}
</script>
<Portal>
<Portal name="flow-module">
<ConfirmationModal
title="Confirm deleting step with dependents"
confirmationText="Delete step"
@@ -1,114 +1,129 @@
<script lang="ts">
import { customIcon } from './store'
export let height = '24px'
export let width = '24px'
export let white = false
export let spin: 'slow' | 'medium' | 'fast' | 'veryfast' | undefined = undefined
</script>
<!-- Generator: Adobe Illustrator 26.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
class={$$props.class}
class:animate-[spin_2s_linear_infinite]={spin === 'veryfast'}
class:animate-[spin_5s_linear_infinite]={spin === 'fast'}
class:animate-[spin_15s_linear_infinite]={spin === 'medium'}
class:animate-[spin_50s_linear_infinite]={spin === 'slow'}
version="1.1"
id="Calque_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
{width}
{height}
viewBox="0 0 256 256"
style="enable-background:new 0 0 256 256;"
xml:space="preserve"
>
<style type="text/css">
.st0 {
fill: #ffffff;
}
.st1 {
opacity: 0.4;
fill: #ffffff;
}
.st2 {
fill: #bcd4fc;
}
.st2-gray {
fill: #cccccc;
}
.st3 {
fill: #3b82f6;
}
.st4 {
fill: #b3b3b3;
}
.st5 {
fill: url(#SVGID_1_);
}
.st6 {
fill: url(#SVGID_00000021089067129159788970000008246765442136188072_);
}
.st7 {
fill: url(#SVGID_00000117639240116366130650000015074833605515028638_);
}
.st8 {
opacity: 0.4;
fill: url(#SVGID_00000101781798616409025840000016567063639337360777_);
}
.st9 {
opacity: 0.4;
fill: url(#SVGID_00000052086836598721292040000002033117744178971046_);
}
.st10 {
opacity: 0.4;
fill: url(#SVGID_00000159460939004760751800000002448009281983951536_);
}
.st11 {
opacity: 0.4;
fill: url(#SVGID_00000013177830667419993080000017721442101626521532_);
}
.st12 {
opacity: 0.4;
fill: url(#SVGID_00000152235521444854938490000006526001119318383285_);
}
.st13 {
opacity: 0.4;
fill: url(#SVGID_00000119823135212293698520000012774889010992664993_);
}
</style>
<g>
<polygon
class:st2={!white}
class:st2-gray={white}
points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22 "
{#if customIcon}
{#if white}
<img src={customIcon.white} alt="Windmill Custom icon" {width} {height} class={$$props.class} />
{:else}
<img
src={customIcon.normal}
alt="Windmill Custom icon"
{width}
{height}
class={$$props.class}
/>
<polygon
class:st3={!white}
class:st0={white}
points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02 "
/>
<polygon
class:st3={!white}
class:st0={white}
points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46
{/if}
{:else}
<svg
class={$$props.class}
class:animate-[spin_2s_linear_infinite]={spin === 'veryfast'}
class:animate-[spin_5s_linear_infinite]={spin === 'fast'}
class:animate-[spin_15s_linear_infinite]={spin === 'medium'}
class:animate-[spin_50s_linear_infinite]={spin === 'slow'}
version="1.1"
id="Calque_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
{width}
{height}
viewBox="0 0 256 256"
style="enable-background:new 0 0 256 256;"
xml:space="preserve"
>
<style type="text/css">
.st0 {
fill: #ffffff;
}
.st1 {
opacity: 0.4;
fill: #ffffff;
}
.st2 {
fill: #bcd4fc;
}
.st2-gray {
fill: #cccccc;
}
.st3 {
fill: #3b82f6;
}
.st4 {
fill: #b3b3b3;
}
.st5 {
fill: url(#SVGID_1_);
}
.st6 {
fill: url(#SVGID_00000021089067129159788970000008246765442136188072_);
}
.st7 {
fill: url(#SVGID_00000117639240116366130650000015074833605515028638_);
}
.st8 {
opacity: 0.4;
fill: url(#SVGID_00000101781798616409025840000016567063639337360777_);
}
.st9 {
opacity: 0.4;
fill: url(#SVGID_00000052086836598721292040000002033117744178971046_);
}
.st10 {
opacity: 0.4;
fill: url(#SVGID_00000159460939004760751800000002448009281983951536_);
}
.st11 {
opacity: 0.4;
fill: url(#SVGID_00000013177830667419993080000017721442101626521532_);
}
.st12 {
opacity: 0.4;
fill: url(#SVGID_00000152235521444854938490000006526001119318383285_);
}
.st13 {
opacity: 0.4;
fill: url(#SVGID_00000119823135212293698520000012774889010992664993_);
}
</style>
<g>
<polygon
class:st2={!white}
class:st2-gray={white}
points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22 "
/>
<polygon
class:st3={!white}
class:st0={white}
points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02 "
/>
<polygon
class:st3={!white}
class:st0={white}
points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46
"
/>
<polygon
class:st3={!white}
class:st0={white}
points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22 "
/>
<polygon
class:st2={!white}
class:st2-gray={white}
points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21 "
/>
<polygon
class:st2={!white}
class:st2-gray={white}
points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46 "
/>
</g>
</svg>
/>
<polygon
class:st3={!white}
class:st0={white}
points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22 "
/>
<polygon
class:st2={!white}
class:st2-gray={white}
points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21 "
/>
<polygon
class:st2={!white}
class:st2-gray={white}
points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46 "
/>
</g>
</svg>
{/if}
@@ -0,0 +1 @@
export let customIcon: { normal: string; white: string } | undefined = undefined
@@ -80,7 +80,7 @@
</div>
</MultiSelect>
</div>
<Portal>
<Portal name="multi-select">
<div use:floatingContent class="z5000" hidden={!open}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -63,7 +63,7 @@
$: fullyCollapsed = keys.length > 1 && collapsed
</script>
<Portal>
<Portal name="object-viewer">
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
</Portal>
@@ -44,7 +44,7 @@
}
</script>
<Portal>
<Portal name="run-row">
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
</Portal>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -105,7 +105,7 @@
<div class="flex flex-row items-center gap-1 text-gray-500 dark:text-gray-300 text-2xs">
{#if job}
{#if 'started_at' in job && job.started_at}
Started <TimeAgo withDate agoOnlyIfRecent date={job.started_at ?? ''} />
Started <TimeAgo agoOnlyIfRecent date={job.started_at ?? ''} />
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
(Ran in {msToReadableTime(
job.duration_ms
@@ -121,11 +121,7 @@
{:else if `scheduled_for` in job && job.scheduled_for && forLater(job.scheduled_for)}
Scheduled for {displayDate(job.scheduled_for)}
{:else}
Waiting for executor (created <TimeAgo
withDate
agoOnlyIfRecent
date={job.created_at || ''}
/>)
Waiting for executor (created <TimeAgo agoOnlyIfRecent date={job.created_at || ''} />)
{/if}
{/if}
</div>
@@ -341,6 +341,7 @@
/>
{:else}
<ArgInput
noDefaultOnSelectFirst
{itemPicker}
resourceTypes={getResourceTypesFromFormat(format)}
bind:value={defaultValue}
@@ -499,7 +499,7 @@
</script>
{#if open}
<Portal>
<Portal name="global-search">
<div
class={twMerge(
`fixed top-0 bottom-0 left-0 right-0 transition-all duration-50 flex items-start justify-center`,
@@ -1,8 +0,0 @@
export default `import os
def main(message: str, name: str, step_id: str):
flow_id = os.environ.get("WM_ROOT_FLOW_JOB_ID")
print("message", message)
print("name", name)
print("step_id", step_id)
return { "message": message, "flow_id": flow_id, "step_id": step_id, "recover": False }`
@@ -1,40 +0,0 @@
export default `import os
import wmill
# You can import any PyPi package.
# See here for more info: https://www.windmill.dev/docs/advanced/dependencies_in_python
# you can use typed resources by doing a type alias to dict
#postgresql = dict
def main(
no_default: str,
#db: postgresql,
name="Nicolas Bourbaki",
age=42,
obj: dict = {"even": "dicts"},
l: list = ["or", "lists!"],
file_: bytes = bytes(0),
):
print(f"Hello World and a warm welcome especially to {name}")
print("and its acolytes..", age, obj, l, len(file_))
# retrieve variables, resources, states using the wmill client
try:
secret = wmill.get_variable("f/examples/secret")
except:
secret = "No secret yet at f/examples/secret !"
print(f"The variable at \`f/examples/secret\`: {secret}")
# Get last state of this script execution by the same trigger/user
last_state = wmill.get_state()
new_state = {"foo": 42} if last_state is None else last_state
new_state["foo"] += 1
wmill.set_state(new_state)
# fetch context variables
user = os.environ.get("WM_USERNAME")
# return value is converted to JSON
return {"splitted": name.split(), "user": user, "state": new_state}`
@@ -1,5 +0,0 @@
export default `# import wmill
def main(x: str):
return x`
@@ -1,14 +0,0 @@
export default `import wmill
def main():
# A common trigger script would follow this pattern:
# 1. Get the last saved state
# state = wmill.get_state()
# 2. Get the actual state from the external service
# newState = ...
# 3. Compare the two states and update the internal state
# wmill.setState(newState)
# 4. Return the new rows
# return range from (state to newState)
return [1, 2, 3]`
+103 -43
View File
@@ -1,19 +1,79 @@
import { type Script } from './gen'
import PYTHON_INIT_CODE from '$lib/init_scripts/python_init_code'
import PYTHON_INIT_CODE_CLEAR from '$lib/init_scripts/python_init_code_clear'
import PYTHON_INIT_CODE_TRIGGER from '$lib/init_scripts/python_init_code_trigger'
import PYTHON_FAILURE_MODULE_CODE from '$lib/init_scripts/python_failure_module'
import type { SupportedLanguage } from './common'
export {
PYTHON_INIT_CODE,
PYTHON_INIT_CODE_CLEAR,
PYTHON_INIT_CODE_TRIGGER,
PYTHON_FAILURE_MODULE_CODE
}
export let PYTHON_FAILURE_MODULE_CODE = `import os
export const NATIVETS_INIT_CODE = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
def main(message: str, name: str, step_id: str):
flow_id = os.environ.get("WM_ROOT_FLOW_JOB_ID")
print("message", message)
print("name", name)
print("step_id", step_id)
return { "message": message, "flow_id": flow_id, "step_id": step_id, "recover": False }`
export let PYTHON_INIT_CODE_CLEAR = `# import wmill
def main(x: str):
return x`
export let PYTHON_INIT_CODE_TRIGGER = `import wmill
def main():
# A common trigger script would follow this pattern:
# 1. Get the last saved state
# state = wmill.get_state()
# 2. Get the actual state from the external service
# newState = ...
# 3. Compare the two states and update the internal state
# wmill.setState(newState)
# 4. Return the new rows
# return range from (state to newState)
return [1, 2, 3]`
export let PYTHON_INIT_CODE = `import os
import wmill
# You can import any PyPi package.
# See here for more info: https://www.windmill.dev/docs/advanced/dependencies_in_python
# you can use typed resources by doing a type alias to dict
#postgresql = dict
def main(
no_default: str,
#db: postgresql,
name="Nicolas Bourbaki",
age=42,
obj: dict = {"even": "dicts"},
l: list = ["or", "lists!"],
file_: bytes = bytes(0),
):
print(f"Hello World and a warm welcome especially to {name}")
print("and its acolytes..", age, obj, l, len(file_))
# retrieve variables, resources, states using the wmill client
try:
secret = wmill.get_variable("f/examples/secret")
except:
secret = "No secret yet at f/examples/secret !"
print(f"The variable at \`f/examples/secret\`: {secret}")
# Get last state of this script execution by the same trigger/user
last_state = wmill.get_state()
new_state = {"foo": 42} if last_state is None else last_state
new_state["foo"] += 1
wmill.set_state(new_state)
# fetch context variables
user = os.environ.get("WM_USERNAME")
# return value is converted to JSON
return {"splitted": name.split(), "user": user, "state": new_state}`
export let NATIVETS_INIT_CODE = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
//import * as wmill from './windmill.ts'
export async function main(example_input: number = 3) {
@@ -25,7 +85,7 @@ export async function main(example_input: number = 3) {
}
`
export const BUNNATIVE_INIT_CODE = `//native
export let BUNNATIVE_INIT_CODE = `//native
//you can add proxy support using //proxy http(s)://host:port
// native scripts are bun scripts that are executed on native workers and can be parallelized
@@ -42,7 +102,7 @@ export async function main(example_input: number = 3) {
}
`
export const NATIVETS_INIT_CODE_CLEAR = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
export let NATIVETS_INIT_CODE_CLEAR = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
//import * as wmill from './windmill.ts'
export async function main() {
@@ -53,7 +113,7 @@ export async function main() {
}
`
export const DENO_INIT_CODE = `// Ctrl/CMD+. to cache dependencies on imports hover.
export let DENO_INIT_CODE = `// Ctrl/CMD+. to cache dependencies on imports hover.
// Deno uses "npm:" prefix to import from npm (https://deno.land/manual@v1.36.3/node/npm_specifiers)
// import * as wmill from "npm:windmill-client@${__pkg__.version}"
@@ -74,7 +134,7 @@ export async function main(
}
`
export const BUN_INIT_CODE = `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
export let BUN_INIT_CODE = `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
// https://www.windmill.dev/docs/getting_started/scripts_quickstart/typescript#modes
// import { toWords } from "number-to-words@1"
@@ -105,7 +165,7 @@ export async function main(
}
`
export const GO_INIT_CODE = `package inner
export let GO_INIT_CODE = `package inner
import (
"fmt"
@@ -129,7 +189,7 @@ func main(x string, nested struct {
}
`
export const GO_FAILURE_MODULE_CODE = `package inner
export let GO_FAILURE_MODULE_CODE = `package inner
import (
"fmt"
@@ -146,21 +206,21 @@ func main(message string, name string) (interface{}, error) {
}
`
export const DENO_INIT_CODE_CLEAR = `// import * as wmill from "npm:windmill-client@${__pkg__.version}"
export let DENO_INIT_CODE_CLEAR = `// import * as wmill from "npm:windmill-client@${__pkg__.version}"
export async function main(x: string) {
return x
}
`
export const BUN_INIT_CODE_CLEAR = `// import * as wmill from "windmill-client"
export let BUN_INIT_CODE_CLEAR = `// import * as wmill from "windmill-client"
export async function main(x: string) {
return x
}
`
export const DENO_FAILURE_MODULE_CODE = `
export let DENO_FAILURE_MODULE_CODE = `
export async function main(message: string, name: string, step_id: string) {
const flow_id = Deno.env.get("WM_ROOT_FLOW_JOB_ID")
console.log("message", message)
@@ -170,7 +230,7 @@ export async function main(message: string, name: string, step_id: string) {
}
`
export const BUN_FAILURE_MODULE_CODE = `
export let BUN_FAILURE_MODULE_CODE = `
export async function main(message: string, name: string, step_id: string) {
const flow_id = process.env.WM_ROOT_FLOW_JOB_ID
console.log("message", message)
@@ -180,7 +240,7 @@ export async function main(message: string, name: string, step_id: string) {
}
`
export const POSTGRES_INIT_CODE = `-- to pin the database use '-- database f/your/path'
export let POSTGRES_INIT_CODE = `-- to pin the database use '-- database f/your/path'
-- $1 name1 = default arg
-- $2 name2
-- $3 name3
@@ -189,7 +249,7 @@ INSERT INTO demo VALUES (\$1::TEXT, \$2::INT, \$3::TEXT[]) RETURNING *;
UPDATE demo SET col2 = \$4::INT WHERE col2 = \$2::INT;
`
export const MYSQL_INIT_CODE = `-- to pin the database use '-- database f/your/path'
export let MYSQL_INIT_CODE = `-- to pin the database use '-- database f/your/path'
-- :name1 (text) = default arg
-- :name2 (int)
-- :name3 (int)
@@ -197,7 +257,7 @@ INSERT INTO demo VALUES (:name1, :name2);
UPDATE demo SET col2 = :name3 WHERE col2 = :name2;
`
export const BIGQUERY_INIT_CODE = `-- to pin the database use '-- database f/your/path'
export let BIGQUERY_INIT_CODE = `-- to pin the database use '-- database f/your/path'
-- @name1 (string) = default arg
-- @name2 (integer)
-- @name3 (string[])
@@ -206,7 +266,7 @@ INSERT INTO \`demodb.demo\` VALUES (@name1, @name2, @name3);
UPDATE \`demodb.demo\` SET col2 = @name4 WHERE col2 = @name2;
`
export const SNOWFLAKE_INIT_CODE = `-- to pin the database use '-- database f/your/path'
export let SNOWFLAKE_INIT_CODE = `-- to pin the database use '-- database f/your/path'
-- ? name1 (varchar) = default arg
-- ? name2 (int)
INSERT INTO demo VALUES (?, ?);
@@ -215,7 +275,7 @@ INSERT INTO demo VALUES (?, ?);
UPDATE demo SET col2 = ? WHERE col2 = ?;
`
export const MSSQL_INIT_CODE = `-- return_last_result
export let MSSQL_INIT_CODE = `-- return_last_result
-- to pin the database use '-- database f/your/path'
-- @p1 name1 (varchar) = default arg
-- @p2 name2 (int)
@@ -224,7 +284,7 @@ INSERT INTO demo VALUES (@p1, @p2);
UPDATE demo SET col2 = @p3 WHERE col2 = @p2;
`
export const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [String]) {
export let GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [String]) {
demo(name1: $name1, name2: $name2, name3: $name3) {
name1,
name2,
@@ -233,7 +293,7 @@ export const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [St
}
`
export const PHP_INIT_CODE = `<?php
export let PHP_INIT_CODE = `<?php
// remove the first // of the following lines to specify packages to install using composer
// // require:
@@ -253,7 +313,7 @@ function main(
}
`
export const RUST_INIT_CODE = `//! Add dependencies in the following partial Cargo.toml manifest
export let RUST_INIT_CODE = `//! Add dependencies in the following partial Cargo.toml manifest
//!
//! \`\`\`cargo
//! [dependencies]
@@ -288,7 +348,7 @@ fn main(who_to_greet: String, numbers: Vec<i8>) -> anyhow::Result<Ret> {
}
`
export const FETCH_INIT_CODE = `export async function main(
export let FETCH_INIT_CODE = `export async function main(
url: string | undefined,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' = 'GET',
body: Object = {},
@@ -319,7 +379,7 @@ export const FETCH_INIT_CODE = `export async function main(
})
}`
export const BASH_INIT_CODE = `# shellcheck shell=bash
export let BASH_INIT_CODE = `# shellcheck shell=bash
# arguments of the form X="$I" are parsed as parameters X of type string
msg="$1"
dflt="\${2:-default value}"
@@ -329,7 +389,7 @@ dflt="\${2:-default value}"
echo "Hello $msg"
`
export const DENO_INIT_CODE_TRIGGER = `import * as wmill from "npm:windmill-client@${__pkg__.version}"
export let DENO_INIT_CODE_TRIGGER = `import * as wmill from "npm:windmill-client@${__pkg__.version}"
export async function main() {
@@ -350,7 +410,7 @@ export async function main() {
}
`
export const BUN_INIT_CODE_TRIGGER = `import * as wmill from "windmill-client"
export let BUN_INIT_CODE_TRIGGER = `import * as wmill from "windmill-client"
export async function main() {
@@ -371,7 +431,7 @@ export async function main() {
}
`
export const GO_INIT_CODE_TRIGGER = `package inner
export let GO_INIT_CODE_TRIGGER = `package inner
import (
wmill "github.com/windmill-labs/windmill-go-client"
@@ -395,7 +455,7 @@ func main() (interface{}, error) {
}
`
export const DENO_INIT_CODE_APPROVAL = `import * as wmill from "npm:windmill-client@^1.158.2"
export let DENO_INIT_CODE_APPROVAL = `import * as wmill from "npm:windmill-client@^1.158.2"
export async function main(approver?: string) {
const urls = await wmill.getResumeUrls(approver)
@@ -423,7 +483,7 @@ export async function main(approver?: string) {
// add a form in Advanced - Suspend
// all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
export const BUN_INIT_CODE_APPROVAL = `import * as wmill from "windmill-client@^1.158.2"
export let BUN_INIT_CODE_APPROVAL = `import * as wmill from "windmill-client@^1.158.2"
export async function main(approver?: string) {
const urls = await wmill.getResumeUrls(approver)
@@ -451,7 +511,7 @@ export async function main(approver?: string) {
// add a form in Advanced - Suspend
// all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
export const BUN_PREPROCESSOR_MODULE_CODE = `
export let BUN_PREPROCESSOR_MODULE_CODE = `
export async function preprocessor(
wm_trigger: {
kind: 'http' | 'email' | 'webhook',
@@ -472,7 +532,7 @@ export async function preprocessor(
}
`
export const DENO_PREPROCESSOR_MODULE_CODE = `
export let DENO_PREPROCESSOR_MODULE_CODE = `
export async function preprocessor(
wm_trigger: {
kind: 'http' | 'email' | 'wehbook',
@@ -493,7 +553,7 @@ export async function preprocessor(
}
`
export const PYTHON_INIT_CODE_APPROVAL = `import wmill
export let PYTHON_INIT_CODE_APPROVAL = `import wmill
def main():
urls = wmill.get_resume_urls()
@@ -520,7 +580,7 @@ def main():
# add a form in Advanced - Suspend
# all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
export const PYTHON_PREPROCESSOR_MODULE_CODE = `from typing import TypedDict, Literal
export let PYTHON_PREPROCESSOR_MODULE_CODE = `from typing import TypedDict, Literal
class Http(TypedDict):
route: str # The route path, e.g. "/users/:id"
@@ -543,7 +603,7 @@ def preprocessor(
}
`
export const DOCKER_INIT_CODE = `# shellcheck shell=bash
export let DOCKER_INIT_CODE = `# shellcheck shell=bash
# Bash script that calls docker as a client to the host daemon
# See documentation: https://www.windmill.dev/docs/advanced/docker
msg="\${1:-world}"
@@ -556,7 +616,7 @@ docker pull $IMAGE
docker run --rm $IMAGE $COMMAND
`
export const POWERSHELL_INIT_CODE = `param($Msg, $Dflt = "default value", [int]$Nb = 3)
export let POWERSHELL_INIT_CODE = `param($Msg, $Dflt = "default value", [int]$Nb = 3)
# Import-Module MyModule
@@ -567,7 +627,7 @@ export const POWERSHELL_INIT_CODE = `param($Msg, $Dflt = "default value", [int]$
# the last line of the stdout is the return value
Write-Output "Hello $Msg"`
export const ANSIBLE_PLAYBOOK_INIT_CODE = `---
export let ANSIBLE_PLAYBOOK_INIT_CODE = `---
inventory:
- resource_type: ansible_inventory
# You can pin an inventory to this script by hardcoding the resource path:
@@ -31,6 +31,7 @@
Badge,
Loader2,
GitFork,
Play,
History,
Columns,
Pen,
@@ -182,6 +183,19 @@
label: `View runs`,
buttonProps: {
href: `${base}/runs/${flow.path}`,
size: 'xs',
color: 'light',
startIcon: Play
}
})
buttons.push({
label: `History`,
buttonProps: {
onClick: () => {
flowHistory?.open()
},
size: 'xs',
color: 'light',
startIcon: History
@@ -442,7 +456,7 @@
{/if}
<div class="flex flex-row gap-x-2 flex-wrap items-center">
<span class="text-sm text-tertiary">
Edited <TimeAgo withDate date={flow.edited_at ?? ''} /> by {flow.edited_by}
Edited <TimeAgo date={flow.edited_at ?? ''} /> by {flow.edited_by}
</span>
{#if flow.archived}
@@ -9,9 +9,7 @@
type WorkflowStatus,
type NewScript,
ConcurrencyGroupsService,
MetricsService
} from '$lib/gen'
import {
canWrite,
@@ -97,7 +95,7 @@
let job: Job | undefined
let jobUpdateLastFetch: Date | undefined
let scriptProgress: number | undefined = undefined;
let scriptProgress: number | undefined = undefined
let currentJobIsLongRunning: boolean = false
let viewTab: 'result' | 'logs' | 'code' | 'stats' = 'result'
@@ -203,20 +201,20 @@
async function onJobLoaded() {
// We want to set up scriptProgress once job is loaded
// We need this to show progress bar if job has progress and is finished
if (job && job.type == "CompletedJob"){
if (job && job.type == 'CompletedJob') {
// If error occured and job is completed
// than we fetch progress from server to display on what progress did it fail
// Could be displayed after run or as a historical page
// If opening page without running job (e.g. reloading page after run) progress will be displayed instantly
MetricsService.getJobProgress({
workspace: job.workspace_id ?? "NO_WORKSPACE",
id: job.id,
}).then(progress => {
// Returned progress is not always 100%, could be 65%, 33%, anything
// Its ok if its a failure and we want to keep that value
// But we want progress to be 100% if job has been succeeded
scriptProgress = progress;
});
workspace: job.workspace_id ?? 'NO_WORKSPACE',
id: job.id
}).then((progress) => {
// Returned progress is not always 100%, could be 65%, 33%, anything
// Its ok if its a failure and we want to keep that value
// But we want progress to be 100% if job has been succeeded
scriptProgress = progress
})
}
if (job === undefined || job.job_kind !== 'script' || job.script_hash === undefined) {
@@ -229,7 +227,6 @@
if (script.restart_unless_cancelled ?? false) {
persistentScriptDefinition = script
}
}
$: {
@@ -349,7 +346,7 @@
<TestJobLoader
lazyLogs
bind:scriptProgress
bind:scriptProgress
on:done={() => job?.['result'] != undefined && (viewTab = 'result')}
bind:this={testJobLoader}
bind:getLogs
@@ -360,7 +357,7 @@
bind:notfound
/>
<Portal>
<Portal name="persistent-run">
<PersistentScriptDrawer bind:this={persistentScriptDrawer} />
</Portal>
@@ -613,11 +610,7 @@
<Button
on:click|once={() => {
$initialArgsStore = job?.args
goto(
`${stem}/edit/${
job?.script_path
}${isScript ? `` : `?nodraft=true`}`
)
goto(`${stem}/edit/${job?.script_path}${isScript ? `` : `?nodraft=true`}`)
}}
color="blue"
size="sm"
@@ -753,7 +746,7 @@
<Skeleton loading={!job} layout={[[9.5]]} />
{#if job}
<FlowMetadata {job} {scheduleEditor} />
{#if currentJobIsLongRunning && showExplicitProgressTip && !scriptProgress && 'running' in job}
{#if currentJobIsLongRunning && showExplicitProgressTip && !scriptProgress && 'running' in job}
<Alert
class="mt-4 p-1 flex flex-row relative text-center"
size="xs"
@@ -801,7 +794,7 @@
/>
{/if}
{#if scriptProgress}
<JobProgressBar {job} {scriptProgress} class="py-4" hideStepTitle={true}/>
<JobProgressBar {job} {scriptProgress} class="py-4" hideStepTitle={true} />
{/if}
<!-- Logs and outputs-->
<div class="mr-2 sm:mr-0 mt-12">
@@ -660,8 +660,7 @@
{/if}
<div class="flex flex-row gap-x-2 flex-wrap items-center">
<span class="text-sm text-tertiary">
Edited <TimeAgo withDate date={script.created_at || ''} /> by {script.created_by ||
'unknown'}
Edited <TimeAgo date={script.created_at || ''} /> by {script.created_by || 'unknown'}
</span>
<Badge small color="gray">
{truncateHash(script?.hash ?? '')}
@@ -629,7 +629,7 @@
}
</script>
<Portal>
<Portal name="workspace-settings">
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={false} fromWorkspaceSettings={true} />
</Portal>
@@ -1053,9 +1053,12 @@
{:else if tab == 'windmill_lfs'}
<div class="flex flex-col gap-4 my-8">
<div class="flex flex-col gap-1">
<div class=" text-primary text-lg font-semibold">Workspace object storage (S3/Azure Blob)</div>
<div class=" text-primary text-lg font-semibold"
>Workspace object storage (S3/Azure Blob)</div
>
<div class="text-tertiary text-xs">
Connect your Windmill workspace to your S3 bucket or your Azure Blob storage to enable users to read and write from S3 without having to have access to the credentials.
Connect your Windmill workspace to your S3 bucket or your Azure Blob storage to enable
users to read and write from S3 without having to have access to the credentials.
<a
href="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#workspace-object-storage"
target="_blank"
@@ -1074,8 +1077,11 @@
<Alert type="info" title="Logs storage is set at the instance level">
This setting is only for storage of large files allowing to upload files directly to
object storage using S3Object and use the wmill sdk to read and write large files backed
by an object storage. Large-scale log management and distributed dependency caching is under <a href="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#instance-object-storage" class="text-blue-500">Instance object storage</a>, set by the superadmins in the
instance settings UI.
by an object storage. Large-scale log management and distributed dependency caching is
under <a
href="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#instance-object-storage"
class="text-blue-500">Instance object storage</a
>, set by the superadmins in the instance settings UI.
</Alert>
{/if}
{#if s3ResourceSettings}
@@ -1,5 +0,0 @@
export function load() {
return {
stuff: { title: 'App Connection' }
}
}