mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-03 16:02:12 +00:00
perf: shrink the module graph that gates first paint in dev (#11373)
* perf: shrink the module graph that gates first paint in dev * docs: drop the stale synchronous-icons claim on the import card * fix: replay search opened before its modal loads, guard lazy icons * fix: only intercept search before load where the modal mounts * perf: mount app-shell modals on first open and keep monaco off the shell * perf: load the icon map on first read, not at module evaluation * fix: report stale chunks with a reload toast, guard the home page against monaco
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { argsToJsonPayload } from '$lib/schema'
|
||||
import JobLoader from '$lib/components/JobLoader.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
|
||||
import LogPanel from '$lib/components/scriptEditor/LogPanel.svelte'
|
||||
import {
|
||||
type CompletedJob,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
Database,
|
||||
Loader2
|
||||
} from 'lucide-svelte'
|
||||
import DucklakeResultPreview from './assets/AssetGraph/DucklakeResultPreview.svelte'
|
||||
import DataTestsResult from './DataTestsResult.svelte'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import DisplayResultControlBar from './DisplayResultControlBar.svelte'
|
||||
@@ -1105,11 +1104,15 @@
|
||||
/>
|
||||
{#if showMaterializedPreview}
|
||||
<div class="border rounded-md h-80 min-h-0 overflow-hidden">
|
||||
<DucklakeResultPreview
|
||||
assetUri={m.materialized}
|
||||
partition={m.partition}
|
||||
class="h-full"
|
||||
/>
|
||||
<!-- Dynamic: the DuckDB preview reaches ~450 modules (data table editor, low-code
|
||||
components), and every page that shows a job result imports this file. -->
|
||||
{#await import('./assets/AssetGraph/DucklakeResultPreview.svelte') then DucklakeResultPreview}
|
||||
<DucklakeResultPreview.default
|
||||
assetUri={m.materialized}
|
||||
partition={m.partition}
|
||||
class="h-full"
|
||||
/>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -307,7 +307,6 @@
|
||||
import { MinusIcon, SearchIcon } from 'lucide-svelte'
|
||||
import { assignObjInPlace, clone } from '$lib/utils'
|
||||
import GenericDropdown from './select/GenericDropdown.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import TaggedTextInput from './TaggedTextInput.svelte'
|
||||
import { DebouncedTempValue, useTransformedSyncedValue } from '$lib/svelte5Utils.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
@@ -885,20 +884,23 @@
|
||||
</div>
|
||||
{:else if filter.type === 'string' && filter.format === 'json'}
|
||||
<div class="px-2 pb-2">
|
||||
<SimpleEditor
|
||||
autofocus={String(value[currentTag!] ?? '').length === 0}
|
||||
lang="json"
|
||||
autoHeight
|
||||
small
|
||||
bind:code={
|
||||
() => String(value[currentTag!] ?? ''),
|
||||
(v) => {
|
||||
setValueForCurrentTag(v ?? '')
|
||||
taggedTextInput?.preventCursorMoveOnNextSync()
|
||||
<!-- Dynamic: monaco would otherwise load with every list page that has a searchbar. -->
|
||||
{#await import('./SimpleEditor.svelte') then SimpleEditor}
|
||||
<SimpleEditor.default
|
||||
autofocus={String(value[currentTag!] ?? '').length === 0}
|
||||
lang="json"
|
||||
autoHeight
|
||||
small
|
||||
bind:code={
|
||||
() => String(value[currentTag!] ?? ''),
|
||||
(v) => {
|
||||
setValueForCurrentTag(v ?? '')
|
||||
taggedTextInput?.preventCursorMoveOnNextSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
class="border border-border-light rounded min-h-[4rem]"
|
||||
/>
|
||||
class="border border-border-light rounded min-h-[4rem]"
|
||||
/>
|
||||
{/await}
|
||||
</div>
|
||||
{:else if filter.type === 'string'}
|
||||
{#if menuItems.length}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { Boxes, FileText, FolderOpen } from 'lucide-svelte'
|
||||
import { appIconComponent } from './icons'
|
||||
import { appIconComponent } from './icons/appIcon.svelte'
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {any} name
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
const hubProjectUrl = $derived(`//${hubHost}/projects/${project.slug}`)
|
||||
|
||||
// Resolved locally rather than fetched: these are Windmill's own bundled icons, so the
|
||||
// card draws them synchronously instead of waiting on the hub — and keeps working on a
|
||||
// hub that refuses uncredentialed reads.
|
||||
// card does not wait on the hub — and keeps working on a hub that refuses uncredentialed
|
||||
// reads.
|
||||
const icons = $derived(
|
||||
project.iconApps
|
||||
.slice(0, 4)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import Select from './select/Select.svelte'
|
||||
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
|
||||
import DropdownV2 from './DropdownV2.svelte'
|
||||
import { appIconComponent } from './icons'
|
||||
import { appIconComponent } from './icons/appIcon.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import RunChart from '$lib/components/RunChart.svelte'
|
||||
|
||||
import JobRunsPreview from '$lib/components/runs/JobRunsPreview.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
import RunsTable from '$lib/components/runs/RunsTable.svelte'
|
||||
@@ -50,9 +49,7 @@
|
||||
import ConcurrentJobsChart from '$lib/components/ConcurrentJobsChart.svelte'
|
||||
import BatchLoadProgress from '$lib/components/BatchLoadProgress.svelte'
|
||||
import { pluralize, MAX_RESOLUTION_BATCH, MAX_RESOLUTION_NOTE_LEN } from '$lib/utils'
|
||||
import BatchReRunOptionsPane, {
|
||||
type BatchReRunOptions
|
||||
} from '$lib/components/runs/BatchReRunOptionsPane.svelte'
|
||||
import type { BatchReRunOptions } from '$lib/components/runs/BatchReRunOptionsPane.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { page } from '$app/state'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
@@ -631,7 +628,9 @@
|
||||
{#if selectedIds[0] === '-'}
|
||||
<div class="p-4">There is no information available for this job</div>
|
||||
{:else}
|
||||
<JobRunsPreview id={selectedIds[0]} workspace={selectedWorkspace} />
|
||||
{#await import('$lib/components/runs/JobRunsPreview.svelte') then JobRunsPreview}
|
||||
<JobRunsPreview.default id={selectedIds[0]} workspace={selectedWorkspace} />
|
||||
{/await}
|
||||
{/if}
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
@@ -1131,27 +1130,33 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if batchRerunOptionsIsOpen}
|
||||
<BatchReRunOptionsPane
|
||||
{selectedIds}
|
||||
onCancel={() => (
|
||||
(batchRerunOptionsIsOpen = false),
|
||||
(manualSelectionMode = undefined)
|
||||
)}
|
||||
onConfirm={async (options) => {
|
||||
await onReRunSelectedJobs(options)
|
||||
}}
|
||||
/>
|
||||
{#await import('$lib/components/runs/BatchReRunOptionsPane.svelte') then BatchReRunOptionsPane}
|
||||
<BatchReRunOptionsPane.default
|
||||
{selectedIds}
|
||||
onCancel={() => (
|
||||
(batchRerunOptionsIsOpen = false),
|
||||
(manualSelectionMode = undefined)
|
||||
)}
|
||||
onConfirm={async (options) => {
|
||||
await onReRunSelectedJobs(options)
|
||||
}}
|
||||
/>
|
||||
{/await}
|
||||
{:else if selectedIds.length === 1}
|
||||
{#if selectedIds[0] === '-'}
|
||||
<div class="p-4">There is no information available for this job</div>
|
||||
{:else}
|
||||
<JobRunsPreview
|
||||
id={selectedIds[0]}
|
||||
workspace={selectedWorkspace}
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
on:filterByWorker={filterByWorker}
|
||||
onResolutionChanged={() => jobsLoader?.loadJobs(true, true)}
|
||||
/>
|
||||
<!-- Dynamic, like the batch re-run pane: both only show once jobs are selected
|
||||
and each statically reaches hundreds of modules (flow graph, monaco). -->
|
||||
{#await import('$lib/components/runs/JobRunsPreview.svelte') then JobRunsPreview}
|
||||
<JobRunsPreview.default
|
||||
id={selectedIds[0]}
|
||||
workspace={selectedWorkspace}
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
on:filterByWorker={filterByWorker}
|
||||
onResolutionChanged={() => jobsLoader?.loadJobs(true, true)}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
{:else if selectedIds.length > 1}
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { WindmillIcon } from './icons'
|
||||
import WindmillIcon from './icons/WindmillIcon.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { Building } from 'lucide-svelte'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { WindmillIcon2 } from './icons'
|
||||
import WindmillIcon2 from './icons/WindmillIcon2.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { Building } from 'lucide-svelte'
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
import { WorkspaceService, type AIProvider, type AIProviderModel } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { base } from '$lib/base'
|
||||
import AIPromptsModal from '$lib/components/settings/AIPromptsModal.svelte'
|
||||
import { getAiChatManager } from './aiChatManagerContext'
|
||||
import { thinkingPreferences } from './thinkingPreferences.svelte'
|
||||
import {
|
||||
@@ -95,6 +94,11 @@
|
||||
// ---- prompt parameters (User / Workspace custom prompts) ----
|
||||
let mode = $derived(aiChatManager.mode)
|
||||
let modalOpen = $state(false)
|
||||
// Sticky, so the prompts modal mounts on first open and then stays for its close animation.
|
||||
let promptsModalRequested = $state(false)
|
||||
$effect(() => {
|
||||
if (modalOpen) promptsModalRequested = true
|
||||
})
|
||||
let modalScope = $state<'user' | 'workspace'>('user')
|
||||
let customPrompts = $state<Record<string, string>>({})
|
||||
let initialPrompt = $state('')
|
||||
@@ -285,20 +289,24 @@
|
||||
|
||||
<ChatModelSettings {config} />
|
||||
|
||||
{#if promptSettings}
|
||||
<AIPromptsModal
|
||||
bind:open={modalOpen}
|
||||
bind:customPrompts
|
||||
scope={modalScope}
|
||||
modes={[activeMode]}
|
||||
readOnly={modalReadOnly}
|
||||
{readOnlyReason}
|
||||
onSave={modalReadOnly ? undefined : save}
|
||||
onReset={reset}
|
||||
{hasChanges}
|
||||
title={modalScope === 'user' ? 'User AI prompt' : 'Workspace AI prompt'}
|
||||
target="body"
|
||||
fixedHeight="sm"
|
||||
settingsHref={isAdmin ? AI_SETTINGS_HREF : undefined}
|
||||
/>
|
||||
{#if promptSettings && promptsModalRequested}
|
||||
<!-- Dynamic, on first open: statically, the prompts modal pulls the whole chat manager into
|
||||
the home page. -->
|
||||
{#await import('$lib/components/settings/AIPromptsModal.svelte') then AIPromptsModal}
|
||||
<AIPromptsModal.default
|
||||
bind:open={modalOpen}
|
||||
bind:customPrompts
|
||||
scope={modalScope}
|
||||
modes={[activeMode]}
|
||||
readOnly={modalReadOnly}
|
||||
{readOnlyReason}
|
||||
onSave={modalReadOnly ? undefined : save}
|
||||
onReset={reset}
|
||||
{hasChanges}
|
||||
title={modalScope === 'user' ? 'User AI prompt' : 'Workspace AI prompt'}
|
||||
target="body"
|
||||
fixedHeight="sm"
|
||||
settingsHref={isAdmin ? AI_SETTINGS_HREF : undefined}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import AiChat from './AIChat.svelte'
|
||||
import SessionsBetaBanner from '$lib/components/sessions/SessionsBetaBanner.svelte'
|
||||
import { zIndexes } from '$lib/zIndexes'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
@@ -127,7 +126,10 @@
|
||||
class={`flex flex-col min-h-0 z-[${zIndexes.aiChat}]`}
|
||||
>
|
||||
<div class="flex-1 min-h-0">
|
||||
<AiChat />
|
||||
<!-- Dynamic: this layout wraps every workspace page, and the chat reaches ~100 modules. -->
|
||||
{#await import('./AIChat.svelte') then AiChat}
|
||||
<AiChat.default />
|
||||
{/await}
|
||||
</div>
|
||||
{#if showSessionsBetaBanner}
|
||||
<SessionsBetaBanner variant="legacy" />
|
||||
|
||||
@@ -1,202 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, tick } from 'svelte'
|
||||
import type { Component } from 'svelte'
|
||||
import { nextAnimationFrame } from '$lib/utils/paint'
|
||||
import { Drawer } from '$lib/components/common'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import ResourceEditorDrawer from '$lib/components/ResourceEditorDrawer.svelte'
|
||||
import VariableEditor from '$lib/components/VariableEditor.svelte'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { registerToolDisplayActionHandler } from './createdResourceActions.svelte'
|
||||
import type { CreatedResourceTriggerKind, ToolDisplayAction } from './shared'
|
||||
import type CreatedResourceActionDrawersInner from './CreatedResourceActionDrawersInner.svelte'
|
||||
|
||||
type DrawerKey = 'schedule' | CreatedResourceTriggerKind
|
||||
type EditorHandle = {
|
||||
openEdit: (path: string, isFlow: boolean) => Promise<void>
|
||||
}
|
||||
type EditorComponent = Component<
|
||||
{
|
||||
useDrawer?: boolean
|
||||
onUpdate?: (path?: string) => void
|
||||
},
|
||||
EditorHandle
|
||||
>
|
||||
type EditorModule = { default: unknown }
|
||||
type DrawerConfig = {
|
||||
label: string
|
||||
load: () => Promise<EditorModule>
|
||||
}
|
||||
type ActiveDrawerState = {
|
||||
id: number
|
||||
key: DrawerKey
|
||||
path: string
|
||||
promise: Promise<EditorModule>
|
||||
}
|
||||
|
||||
const DRAWER_SIZE = '800px'
|
||||
|
||||
let drawer: Drawer | undefined = $state(undefined)
|
||||
let editor: EditorHandle | undefined = $state(undefined)
|
||||
let activeDrawer: ActiveDrawerState | undefined = $state(undefined)
|
||||
let nextActiveDrawerId = 0
|
||||
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state(undefined)
|
||||
let variableEditor: VariableEditor | undefined = $state(undefined)
|
||||
|
||||
const drawerConfigs: Record<DrawerKey, DrawerConfig> = {
|
||||
schedule: {
|
||||
label: 'schedule',
|
||||
load: () => import('$lib/components/triggers/schedules/ScheduleEditorInner.svelte')
|
||||
},
|
||||
http: {
|
||||
label: 'HTTP trigger',
|
||||
load: () => import('$lib/components/triggers/http/RouteEditorInner.svelte')
|
||||
},
|
||||
websocket: {
|
||||
label: 'WebSocket trigger',
|
||||
load: () => import('$lib/components/triggers/websocket/WebsocketTriggerEditorInner.svelte')
|
||||
},
|
||||
postgres: {
|
||||
label: 'Postgres trigger',
|
||||
load: () => import('$lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte')
|
||||
},
|
||||
kafka: {
|
||||
label: 'Kafka trigger',
|
||||
load: () => import('$lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte')
|
||||
},
|
||||
nats: {
|
||||
label: 'NATS trigger',
|
||||
load: () => import('$lib/components/triggers/nats/NatsTriggerEditorInner.svelte')
|
||||
},
|
||||
mqtt: {
|
||||
label: 'MQTT trigger',
|
||||
load: () => import('$lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte')
|
||||
},
|
||||
amqp: {
|
||||
label: 'AMQP trigger',
|
||||
load: () => import('$lib/components/triggers/amqp/AmqpTriggerEditorInner.svelte')
|
||||
},
|
||||
sqs: {
|
||||
label: 'SQS trigger',
|
||||
load: () => import('$lib/components/triggers/sqs/SqsTriggerEditorInner.svelte')
|
||||
},
|
||||
gcp: {
|
||||
label: 'GCP Pub/Sub trigger',
|
||||
load: () => import('$lib/components/triggers/gcp/GcpTriggerEditorInner.svelte')
|
||||
},
|
||||
azure: {
|
||||
label: 'Azure Event Grid trigger',
|
||||
load: () => import('$lib/components/triggers/azure/AzureTriggerEditorInner.svelte')
|
||||
},
|
||||
email: {
|
||||
label: 'Email trigger',
|
||||
load: () => import('$lib/components/triggers/email/EmailTriggerEditorInner.svelte')
|
||||
}
|
||||
}
|
||||
|
||||
const drawerTitle = $derived.by(() => {
|
||||
const current = activeDrawer
|
||||
return current ? `Edit ${drawerConfigs[current.key].label} ${current.path}` : 'Edit trigger'
|
||||
// The handler registers during render, so the action is offered right away and a page
|
||||
// that takes it over (the sessions panel) registers on top of it. The drawers themselves
|
||||
// load on first use: they reach the resource and variable editors.
|
||||
let inner: Promise<typeof import('./CreatedResourceActionDrawersInner.svelte')> | undefined =
|
||||
$state()
|
||||
let drawers: CreatedResourceActionDrawersInner | undefined = $state()
|
||||
let resolveBound: (d: CreatedResourceActionDrawersInner) => void = () => {}
|
||||
const bound = new Promise<CreatedResourceActionDrawersInner>((r) => (resolveBound = r))
|
||||
$effect(() => {
|
||||
if (drawers) resolveBound(drawers)
|
||||
})
|
||||
|
||||
async function getEditor(request: ActiveDrawerState): Promise<EditorHandle | undefined> {
|
||||
await request.promise
|
||||
await tick()
|
||||
await nextAnimationFrame()
|
||||
|
||||
if (activeDrawer?.id !== request.id) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!editor) {
|
||||
await tick()
|
||||
await nextAnimationFrame()
|
||||
}
|
||||
|
||||
if (!editor) {
|
||||
throw new Error(`${drawerConfigs[request.key].label} drawer is not ready`)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
|
||||
async function openCreatedResource(action: ToolDisplayAction) {
|
||||
if (action.type !== 'open_created_resource') {
|
||||
return
|
||||
}
|
||||
|
||||
if (action.resource === 'resource') {
|
||||
await resourceEditorDrawer?.initEdit(action.path)
|
||||
return
|
||||
}
|
||||
|
||||
if (action.resource === 'variable') {
|
||||
await variableEditor?.editVariable(action.path)
|
||||
return
|
||||
}
|
||||
|
||||
const key = action.resource === 'schedule' ? 'schedule' : action.triggerKind
|
||||
if (!key) {
|
||||
throw new Error('Missing trigger kind')
|
||||
}
|
||||
|
||||
if (activeDrawer?.key === key && activeDrawer.path === action.path && drawer?.isOpen()) {
|
||||
activeDrawer = undefined
|
||||
editor = undefined
|
||||
drawer.closeDrawer()
|
||||
return
|
||||
}
|
||||
|
||||
const config = drawerConfigs[key]
|
||||
const promise = activeDrawer?.key === key ? activeDrawer.promise : config.load()
|
||||
if (activeDrawer?.key !== key) {
|
||||
editor = undefined
|
||||
}
|
||||
|
||||
const request: ActiveDrawerState = {
|
||||
id: nextActiveDrawerId++,
|
||||
key,
|
||||
path: action.path,
|
||||
promise
|
||||
}
|
||||
activeDrawer = request
|
||||
drawer?.openDrawer()
|
||||
|
||||
const currentEditor = await getEditor(request)
|
||||
if (!currentEditor) {
|
||||
return
|
||||
}
|
||||
await currentEditor.openEdit(action.path, action.targetKind === 'flow')
|
||||
}
|
||||
|
||||
function onInnerUpdate() {
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
|
||||
const unregisterCreatedResourceHandler = registerToolDisplayActionHandler(
|
||||
'open_created_resource',
|
||||
openCreatedResource
|
||||
)
|
||||
const unregister = registerToolDisplayActionHandler('open_created_resource', async (action) => {
|
||||
inner ??= import('./CreatedResourceActionDrawersInner.svelte')
|
||||
await inner
|
||||
await (await bound).openCreatedResource(action)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
unregisterCreatedResourceHandler()
|
||||
unregister()
|
||||
})
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size={DRAWER_SIZE}>
|
||||
<DrawerContent title={drawerTitle} on:close={() => drawer?.closeDrawer()}>
|
||||
{#if activeDrawer}
|
||||
{#key activeDrawer.key}
|
||||
{#await activeDrawer.promise}
|
||||
<div class="p-4 flex justify-center">
|
||||
<Loader2 class="animate-spin" />
|
||||
</div>
|
||||
{:then Module}
|
||||
{@const Editor = Module.default as EditorComponent}
|
||||
<Editor bind:this={editor} useDrawer={false} onUpdate={onInnerUpdate} />
|
||||
{/await}
|
||||
{/key}
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<ResourceEditorDrawer bind:this={resourceEditorDrawer} />
|
||||
<VariableEditor bind:this={variableEditor} />
|
||||
{#if inner}
|
||||
{#await inner then Inner}
|
||||
<Inner.default bind:this={drawers} />
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte'
|
||||
import type { Component } from 'svelte'
|
||||
import { nextAnimationFrame } from '$lib/utils/paint'
|
||||
import { Drawer } from '$lib/components/common'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import ResourceEditorDrawer from '$lib/components/ResourceEditorDrawer.svelte'
|
||||
import VariableEditor from '$lib/components/VariableEditor.svelte'
|
||||
import type { CreatedResourceTriggerKind, ToolDisplayAction } from './shared'
|
||||
|
||||
type DrawerKey = 'schedule' | CreatedResourceTriggerKind
|
||||
type EditorHandle = {
|
||||
openEdit: (path: string, isFlow: boolean) => Promise<void>
|
||||
}
|
||||
type EditorComponent = Component<
|
||||
{
|
||||
useDrawer?: boolean
|
||||
onUpdate?: (path?: string) => void
|
||||
},
|
||||
EditorHandle
|
||||
>
|
||||
type EditorModule = { default: unknown }
|
||||
type DrawerConfig = {
|
||||
label: string
|
||||
load: () => Promise<EditorModule>
|
||||
}
|
||||
type ActiveDrawerState = {
|
||||
id: number
|
||||
key: DrawerKey
|
||||
path: string
|
||||
promise: Promise<EditorModule>
|
||||
}
|
||||
|
||||
const DRAWER_SIZE = '800px'
|
||||
|
||||
let drawer: Drawer | undefined = $state(undefined)
|
||||
let editor: EditorHandle | undefined = $state(undefined)
|
||||
let activeDrawer: ActiveDrawerState | undefined = $state(undefined)
|
||||
let nextActiveDrawerId = 0
|
||||
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state(undefined)
|
||||
let variableEditor: VariableEditor | undefined = $state(undefined)
|
||||
|
||||
const drawerConfigs: Record<DrawerKey, DrawerConfig> = {
|
||||
schedule: {
|
||||
label: 'schedule',
|
||||
load: () => import('$lib/components/triggers/schedules/ScheduleEditorInner.svelte')
|
||||
},
|
||||
http: {
|
||||
label: 'HTTP trigger',
|
||||
load: () => import('$lib/components/triggers/http/RouteEditorInner.svelte')
|
||||
},
|
||||
websocket: {
|
||||
label: 'WebSocket trigger',
|
||||
load: () => import('$lib/components/triggers/websocket/WebsocketTriggerEditorInner.svelte')
|
||||
},
|
||||
postgres: {
|
||||
label: 'Postgres trigger',
|
||||
load: () => import('$lib/components/triggers/postgres/PostgresTriggerEditorInner.svelte')
|
||||
},
|
||||
kafka: {
|
||||
label: 'Kafka trigger',
|
||||
load: () => import('$lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte')
|
||||
},
|
||||
nats: {
|
||||
label: 'NATS trigger',
|
||||
load: () => import('$lib/components/triggers/nats/NatsTriggerEditorInner.svelte')
|
||||
},
|
||||
mqtt: {
|
||||
label: 'MQTT trigger',
|
||||
load: () => import('$lib/components/triggers/mqtt/MqttTriggerEditorInner.svelte')
|
||||
},
|
||||
amqp: {
|
||||
label: 'AMQP trigger',
|
||||
load: () => import('$lib/components/triggers/amqp/AmqpTriggerEditorInner.svelte')
|
||||
},
|
||||
sqs: {
|
||||
label: 'SQS trigger',
|
||||
load: () => import('$lib/components/triggers/sqs/SqsTriggerEditorInner.svelte')
|
||||
},
|
||||
gcp: {
|
||||
label: 'GCP Pub/Sub trigger',
|
||||
load: () => import('$lib/components/triggers/gcp/GcpTriggerEditorInner.svelte')
|
||||
},
|
||||
azure: {
|
||||
label: 'Azure Event Grid trigger',
|
||||
load: () => import('$lib/components/triggers/azure/AzureTriggerEditorInner.svelte')
|
||||
},
|
||||
email: {
|
||||
label: 'Email trigger',
|
||||
load: () => import('$lib/components/triggers/email/EmailTriggerEditorInner.svelte')
|
||||
}
|
||||
}
|
||||
|
||||
const drawerTitle = $derived.by(() => {
|
||||
const current = activeDrawer
|
||||
return current ? `Edit ${drawerConfigs[current.key].label} ${current.path}` : 'Edit trigger'
|
||||
})
|
||||
|
||||
async function getEditor(request: ActiveDrawerState): Promise<EditorHandle | undefined> {
|
||||
await request.promise
|
||||
await tick()
|
||||
await nextAnimationFrame()
|
||||
|
||||
if (activeDrawer?.id !== request.id) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!editor) {
|
||||
await tick()
|
||||
await nextAnimationFrame()
|
||||
}
|
||||
|
||||
if (!editor) {
|
||||
throw new Error(`${drawerConfigs[request.key].label} drawer is not ready`)
|
||||
}
|
||||
return editor
|
||||
}
|
||||
|
||||
export async function openCreatedResource(action: ToolDisplayAction) {
|
||||
if (action.type !== 'open_created_resource') {
|
||||
return
|
||||
}
|
||||
|
||||
if (action.resource === 'resource') {
|
||||
await resourceEditorDrawer?.initEdit(action.path)
|
||||
return
|
||||
}
|
||||
|
||||
if (action.resource === 'variable') {
|
||||
await variableEditor?.editVariable(action.path)
|
||||
return
|
||||
}
|
||||
|
||||
const key = action.resource === 'schedule' ? 'schedule' : action.triggerKind
|
||||
if (!key) {
|
||||
throw new Error('Missing trigger kind')
|
||||
}
|
||||
|
||||
if (activeDrawer?.key === key && activeDrawer.path === action.path && drawer?.isOpen()) {
|
||||
activeDrawer = undefined
|
||||
editor = undefined
|
||||
drawer.closeDrawer()
|
||||
return
|
||||
}
|
||||
|
||||
const config = drawerConfigs[key]
|
||||
const promise = activeDrawer?.key === key ? activeDrawer.promise : config.load()
|
||||
if (activeDrawer?.key !== key) {
|
||||
editor = undefined
|
||||
}
|
||||
|
||||
const request: ActiveDrawerState = {
|
||||
id: nextActiveDrawerId++,
|
||||
key,
|
||||
path: action.path,
|
||||
promise
|
||||
}
|
||||
activeDrawer = request
|
||||
drawer?.openDrawer()
|
||||
|
||||
const currentEditor = await getEditor(request)
|
||||
if (!currentEditor) {
|
||||
return
|
||||
}
|
||||
await currentEditor.openEdit(action.path, action.targetKind === 'flow')
|
||||
}
|
||||
|
||||
function onInnerUpdate() {
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size={DRAWER_SIZE}>
|
||||
<DrawerContent title={drawerTitle} on:close={() => drawer?.closeDrawer()}>
|
||||
{#if activeDrawer}
|
||||
{#key activeDrawer.key}
|
||||
{#await activeDrawer.promise}
|
||||
<div class="p-4 flex justify-center">
|
||||
<Loader2 class="animate-spin" />
|
||||
</div>
|
||||
{:then Module}
|
||||
{@const Editor = Module.default as EditorComponent}
|
||||
<Editor bind:this={editor} useDrawer={false} onUpdate={onInnerUpdate} />
|
||||
{/await}
|
||||
{/key}
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<ResourceEditorDrawer bind:this={resourceEditorDrawer} />
|
||||
<VariableEditor bind:this={variableEditor} />
|
||||
@@ -60,7 +60,7 @@ import { logFeatureUsage, logHubScriptPick } from '$lib/utils/featureUsage'
|
||||
import { forLater } from '$lib/forLater'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
import { getCurrentModel } from '$lib/aiStore'
|
||||
import { type editor as meditor } from 'monaco-editor'
|
||||
import type { editor as meditor } from 'monaco-editor'
|
||||
import { pendingFolderInstructions, type FolderInstructionsContext } from './folderInstructions'
|
||||
|
||||
// Prettify function for code arguments - extracts and formats code from JSON
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLongHash } from '$lib/editorLangUtils'
|
||||
import { type editor as meditor } from 'monaco-editor'
|
||||
import type { editor as meditor } from 'monaco-editor'
|
||||
|
||||
export type VisualChange =
|
||||
| {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Alert, Badge, ButtonType, Skeleton } from '$lib/components/common'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import { APP_TO_ICON_COMPONENT } from '$lib/components/icons'
|
||||
import { appIconMap } from '$lib/components/icons/appIcon.svelte'
|
||||
import { listHubIntegrationsShared } from '$lib/components/displayNameLoaders'
|
||||
import ListFilters from '$lib/components/home/ListFilters.svelte'
|
||||
import { ScriptService, type HubScriptKind } from '$lib/gen'
|
||||
@@ -205,8 +205,8 @@
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex justify-center items-center">
|
||||
{#if item['app'] in APP_TO_ICON_COMPONENT}
|
||||
{@const SvelteComponent = APP_TO_ICON_COMPONENT[item['app']]}
|
||||
{#if appIconMap()?.[item['app']]}
|
||||
{@const SvelteComponent = appIconMap()![item['app']]}
|
||||
<SvelteComponent height={18} width={18} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
import { createEventDispatcher, getContext, untrack } from 'svelte'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import { classNames, createCache } from '$lib/utils'
|
||||
import { APP_TO_ICON_COMPONENT } from '$lib/components/icons'
|
||||
import { appIconMap } from '$lib/components/icons/appIcon.svelte'
|
||||
import { listHubIntegrationsShared } from '$lib/components/displayNameLoaders'
|
||||
import { ScriptService, type HubScriptKind } from '$lib/gen'
|
||||
import { Circle, ExternalLink } from 'lucide-svelte'
|
||||
@@ -253,8 +253,8 @@
|
||||
onClick={() => handlePickScript(item)}
|
||||
>
|
||||
<div class={classNames('flex justify-center items-center')}>
|
||||
{#if item['app'] in APP_TO_ICON_COMPONENT}
|
||||
{@const SvelteComponent = APP_TO_ICON_COMPONENT[item['app']]}
|
||||
{#if appIconMap()?.[item['app']]}
|
||||
{@const SvelteComponent = appIconMap()![item['app']]}
|
||||
<SvelteComponent height={13} width={13} />
|
||||
{:else}
|
||||
<div class="text-gray-400 flex flex-row items-center justify-center">
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
import TriggersNode from './renderers/nodes/TriggersNode.svelte'
|
||||
import { Alert, Drawer } from '../common'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import FlowYamlEditor from '../flows/header/FlowYamlEditor.svelte'
|
||||
import BranchOneEndNode from './renderers/nodes/branchOneEndNode.svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import SubflowBound from './renderers/nodes/SubflowBound.svelte'
|
||||
@@ -1296,7 +1295,10 @@
|
||||
</script>
|
||||
|
||||
{#if insertable}
|
||||
<FlowYamlEditor bind:drawer={yamlEditorDrawer} />
|
||||
<!-- Dynamic: the YAML editor is monaco, which read-only graphs (run previews) never need. -->
|
||||
{#await import('../flows/header/FlowYamlEditor.svelte') then FlowYamlEditor}
|
||||
<FlowYamlEditor.default bind:drawer={yamlEditorDrawer} />
|
||||
{/await}
|
||||
{/if}
|
||||
{#if canUseDiffDrawer}
|
||||
<DiffDrawer bind:this={diffDrawer} />
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
import NoItemFound from './NoItemFound.svelte'
|
||||
import WorkspaceEmptyState from './WorkspaceEmptyState.svelte'
|
||||
import HubProjectPickerModal from './HubProjectPickerModal.svelte'
|
||||
import ImportProjectModal from './ImportProjectModal.svelte'
|
||||
import type { HubProjectPick } from '$lib/hubProject'
|
||||
import ListFilters from './ListFilters.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
@@ -1009,6 +1008,11 @@
|
||||
// the create menu's Import section open the same dialog, and mounting one per entry point
|
||||
// would put two of them on the page at once while the workspace is still empty.
|
||||
let hubPick = $state<HubProjectPick | undefined>(undefined)
|
||||
// Sticky, so the import modal mounts on the first pick and then stays for its close animation.
|
||||
let hubImportRequested = $state(false)
|
||||
$effect(() => {
|
||||
if (hubPick) hubImportRequested = true
|
||||
})
|
||||
let hubPickerOpen = $state(false)
|
||||
|
||||
/**
|
||||
@@ -2077,7 +2081,13 @@
|
||||
hubPick = project
|
||||
}}
|
||||
/>
|
||||
<ImportProjectModal pick={hubPick} onClose={() => (hubPick = undefined)} {onImported} />
|
||||
<!-- Dynamic, on the first pick: the import flow statically reaches ~400 modules (resource
|
||||
setup, SQL editor). -->
|
||||
{#if hubImportRequested}
|
||||
{#await import('./ImportProjectModal.svelte') then ImportProjectModal}
|
||||
<ImportProjectModal.default pick={hubPick} onClose={() => (hubPick = undefined)} {onImported} />
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Rows arriving after an import, one after another. The animation is declared on the
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Folder, User } from 'lucide-svelte'
|
||||
import { Badge } from '../common'
|
||||
import type { BadgeColor, BadgeIconProps } from '../common/badge/model'
|
||||
import { appIconComponent } from '../icons'
|
||||
import { appIconComponent } from '../icons/appIcon.svelte'
|
||||
import { integrationDisplayName } from '../resourceTypeDisplay'
|
||||
import { untrack } from 'svelte'
|
||||
import { useHostedPage } from '../hostedPage'
|
||||
@@ -115,7 +115,9 @@
|
||||
<Icon class="mr-0.5" size={12} />
|
||||
{:else if resourceType}
|
||||
{@const SvelteComponent = appIconComponent(filter)}
|
||||
<SvelteComponent height="14px" width="14px" />
|
||||
{#if SvelteComponent}
|
||||
<SvelteComponent height="14px" width="14px" />
|
||||
{/if}
|
||||
{:else if filter.startsWith('u/')}
|
||||
<User class="mr-0.5" size={14} />
|
||||
{:else if filter.startsWith('f/')}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Folder, User, Circle } from 'lucide-svelte'
|
||||
import { appIconComponent } from '../icons'
|
||||
import { appIconComponent } from '../icons/appIcon.svelte'
|
||||
import { integrationDisplayName } from '../resourceTypeDisplay'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button } from '../common'
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Component } from 'svelte'
|
||||
|
||||
// The ~300 icon modules load on first use rather than up front: statically reached from
|
||||
// the app shell, they gate every page load in dev, and started at module evaluation they
|
||||
// compete with the page's own chunks and delay Home's first content. Everything here returns
|
||||
// `undefined` until they have loaded and then re-runs reactive readers — call from markup or
|
||||
// a `$derived`, not once into a plain variable.
|
||||
let icons = $state.raw<typeof import('./index')>()
|
||||
let loading: Promise<unknown> | undefined
|
||||
|
||||
function load() {
|
||||
if (!icons)
|
||||
loading ??= import('./index').then(
|
||||
(m) => (icons = m),
|
||||
// Cleared so the next read retries rather than keeping the tab icon-less; rethrown
|
||||
// so the root layout reports a chunk that failed to load.
|
||||
(e) => {
|
||||
loading = undefined
|
||||
throw e
|
||||
}
|
||||
)
|
||||
return icons
|
||||
}
|
||||
|
||||
export function appIconComponent(name: string | undefined): Component | undefined {
|
||||
if (!name) return undefined
|
||||
return load()?.appIconComponent(name)
|
||||
}
|
||||
|
||||
export function appIconMap(): Record<string, Component> | undefined {
|
||||
return load()?.APP_TO_ICON_COMPONENT
|
||||
}
|
||||
@@ -32,7 +32,6 @@
|
||||
import DraftBadge from '$lib/components/DraftBadge.svelte'
|
||||
import InheritedLabels from '$lib/components/InheritedLabels.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import SupabaseConnect from '$lib/components/SupabaseConnect.svelte'
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
@@ -76,7 +75,6 @@
|
||||
} from 'lucide-svelte'
|
||||
import { onMount, untrack } from 'svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
import EditableSchemaWrapper from '$lib/components/schema/EditableSchemaWrapper.svelte'
|
||||
import ResourceEditorDrawer from '$lib/components/ResourceEditorDrawer.svelte'
|
||||
import {
|
||||
agentEditorTarget,
|
||||
@@ -820,12 +818,14 @@
|
||||
|
||||
<Drawer bind:this={inferrer} size="800px">
|
||||
<DrawerContent title="Infer type from JSON" on:close={() => inferrer?.toggleDrawer?.()}>
|
||||
<SimpleEditor
|
||||
bind:code={inferrerJson}
|
||||
lang="json"
|
||||
class="h-full"
|
||||
fixedOverflowWidgets={false}
|
||||
/>
|
||||
{#await import('$lib/components/SimpleEditor.svelte') then SimpleEditor}
|
||||
<SimpleEditor.default
|
||||
bind:code={inferrerJson}
|
||||
lang="json"
|
||||
class="h-full"
|
||||
fixedOverflowWidgets={false}
|
||||
/>
|
||||
{/await}
|
||||
{#snippet actions()}
|
||||
<Button unifiedSize="sm" on:click={inferJson}>Infer</Button>
|
||||
{/snippet}
|
||||
@@ -920,7 +920,10 @@
|
||||
{:else}
|
||||
<div class="mb-1 font-semibold text-emphasis text-xs">Schema</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<EditableSchemaWrapper bind:schema={editResourceType.schema} noPreview />
|
||||
<!-- Dynamic, like the one below: the schema editor reaches monaco. -->
|
||||
{#await import('$lib/components/schema/EditableSchemaWrapper.svelte') then EditableSchemaWrapper}
|
||||
<EditableSchemaWrapper.default bind:schema={editResourceType.schema} noPreview />
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1008,12 +1011,14 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<EditableSchemaWrapper
|
||||
bind:schema={newResourceType.schema}
|
||||
bind:formatExtension={newResourceType.formatExtension}
|
||||
bind:isFileset={newResourceType.isFileset}
|
||||
fullHeight
|
||||
/>
|
||||
{#await import('$lib/components/schema/EditableSchemaWrapper.svelte') then EditableSchemaWrapper}
|
||||
<EditableSchemaWrapper.default
|
||||
bind:schema={newResourceType.schema}
|
||||
bind:formatExtension={newResourceType.formatExtension}
|
||||
bind:isFileset={newResourceType.isFileset}
|
||||
fullHeight
|
||||
/>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import QuickMenuItem from './QuickMenuItem.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { displayDateOnly } from '$lib/utils'
|
||||
import JobPreview from '../runs/JobRunsPreview.svelte'
|
||||
|
||||
let debounceTimeout: any = undefined
|
||||
const debouncePeriod: number = 1000
|
||||
@@ -210,7 +209,11 @@
|
||||
Select a result to preview
|
||||
{:else}
|
||||
<div class="h-[95%] overflow-y-scroll">
|
||||
<JobPreview id={selectedItem?.document?.id[0]} workspace={selectedWorkspace} />
|
||||
<!-- Dynamic: the job preview reaches the flow graph and monaco, and this search is
|
||||
part of the global search modal. -->
|
||||
{#await import('../runs/JobRunsPreview.svelte') then JobPreview}
|
||||
<JobPreview.default id={selectedItem?.document?.id[0]} workspace={selectedWorkspace} />
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row pt-3 pl-4 items-center text-xs text-secondary">
|
||||
@@ -246,8 +249,9 @@
|
||||
<div class="text-sm">
|
||||
Note that new runs might take a while to become searchable (by default ~5min).
|
||||
{#if indexMetadata?.max_index_time_window_secs}
|
||||
Search only covers the last {Math.round(indexMetadata.max_index_time_window_secs / 86400)} day(s),
|
||||
configurable in instance settings under Indexer.
|
||||
Search only covers the last {Math.round(
|
||||
indexMetadata.max_index_time_window_secs / 86400
|
||||
)} day(s), configurable in instance settings under Indexer.
|
||||
{/if}
|
||||
</div>
|
||||
{#if !$enterpriseLicense}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import { Button } from '$lib/components/common'
|
||||
import Modal from '$lib/components/common/modal/Modal.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import { GithubIcon, GitlabIcon, GoogleIcon } from '$lib/components/icons'
|
||||
import GithubIcon from '$lib/components/icons/GithubIcon.svelte'
|
||||
import GitlabIcon from '$lib/components/icons/GitlabIcon.svelte'
|
||||
import GoogleIcon from '$lib/components/icons/GoogleIcon.svelte'
|
||||
import { OauthService } from '$lib/gen'
|
||||
|
||||
// An account created through a pre-approved invite has no credentials yet
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { page } from '$app/stores'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { buildWsUrl } from '$lib/wsUrl'
|
||||
import { signMultiplayerRequest } from '$lib/components/debug'
|
||||
import { signMultiplayerRequest } from '$lib/components/debug/debugUtils'
|
||||
|
||||
let awareness: Awareness | undefined = $state(undefined)
|
||||
let wsProvider: WebsocketProvider | undefined = undefined
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// lucide glyphs, where a coloured brand mark reads as a different kind of thing.
|
||||
import { triggerIconMapMono as triggerIconMap, type TriggerType } from './utils'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import { SchedulePollIcon } from '$lib/components/icons'
|
||||
import SchedulePollIcon from '$lib/components/icons/SchedulePollIcon.svelte'
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { CloudOff } from 'lucide-svelte'
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
import { isObject, sendUserToast } from '$lib/utils'
|
||||
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
|
||||
import type { Capture } from '$lib/gen'
|
||||
import { AwsIcon, MqttIcon, AmqpIcon } from '../icons'
|
||||
import AwsIcon from '../icons/AwsIcon.svelte'
|
||||
import MqttIcon from '../icons/MqttIcon.svelte'
|
||||
import AmqpIcon from '../icons/AmqpIcon.svelte'
|
||||
import GoogleCloudIcon from '../icons/GoogleCloudIcon.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { tickPainted } from '$lib/utils/paint'
|
||||
import ScheduleEditorInner from './ScheduleEditorInner.svelte'
|
||||
import type ScheduleEditorInner from './ScheduleEditorInner.svelte'
|
||||
|
||||
let { onUpdate }: { onUpdate?: (path?: string) => void } = $props()
|
||||
|
||||
// Dynamic: every script and flow row embeds this wrapper, and the editor statically
|
||||
// reaches ~280 modules (script picker, flow graph, ...).
|
||||
let inner: Promise<typeof import('./ScheduleEditorInner.svelte')> | undefined
|
||||
const loadInner = () => (inner ??= import('./ScheduleEditorInner.svelte'))
|
||||
|
||||
let open = $state(false)
|
||||
export async function openEdit(ePath: string, isFlow: boolean, fixedScriptPath?: string) {
|
||||
open = true
|
||||
await loadInner()
|
||||
await tickPainted()
|
||||
drawer?.openEdit(ePath, isFlow, undefined, fixedScriptPath)
|
||||
}
|
||||
@@ -18,6 +24,7 @@
|
||||
fixedScriptPath?: string
|
||||
) {
|
||||
open = true
|
||||
await loadInner()
|
||||
await tickPainted()
|
||||
drawer?.openNew(is_flow, initial_script_path, undefined, schedule_path, fixedScriptPath)
|
||||
}
|
||||
@@ -26,5 +33,7 @@
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<ScheduleEditorInner {onUpdate} bind:this={drawer} />
|
||||
{#await loadInner() then Inner}
|
||||
<Inner.default {onUpdate} bind:this={drawer} />
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import SettingsPageHeader from '$lib/components/settings/SettingsPageHeader.svelte'
|
||||
import { Check, X, ExternalLink, Cog, Plug } from 'lucide-svelte'
|
||||
import { NextcloudIcon, GithubIcon } from '$lib/components/icons'
|
||||
import NextcloudIcon from '$lib/components/icons/NextcloudIcon.svelte'
|
||||
import GithubIcon from '$lib/components/icons/GithubIcon.svelte'
|
||||
import GoogleIcon from '$lib/components/icons/GoogleIcon.svelte'
|
||||
import { WorkspaceIntegrationService, type NativeServiceName } from '$lib/gen'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('./gen', () => ({ HubPublishService: {}, SettingService: {} }))
|
||||
vi.mock('./components/icons', () => ({ appIconComponent: () => undefined }))
|
||||
vi.mock('./components/icons/appIcon.svelte', () => ({ appIconComponent: () => undefined }))
|
||||
|
||||
import { hubProjectDescription, pinHubProjects } from './hubProject'
|
||||
|
||||
@@ -52,11 +52,27 @@ describe('hubProjectDescription', () => {
|
||||
|
||||
describe('pinHubProjects', () => {
|
||||
const pick = (slug: string, apps: string[], stars: number) =>
|
||||
({ id: slug, slug, name: slug, summary: '', description: '', author: '', apps, iconApps: apps, stars }) as any
|
||||
const all = [pick('a', ['slack'], 9), pick('b', ['hubspot'], 5), pick('c', ['hubspot', 'slack'], 2)]
|
||||
({
|
||||
id: slug,
|
||||
slug,
|
||||
name: slug,
|
||||
summary: '',
|
||||
description: '',
|
||||
author: '',
|
||||
apps,
|
||||
iconApps: apps,
|
||||
stars
|
||||
}) as any
|
||||
const all = [
|
||||
pick('a', ['slack'], 9),
|
||||
pick('b', ['hubspot'], 5),
|
||||
pick('c', ['hubspot', 'slack'], 2)
|
||||
]
|
||||
|
||||
it('hoists the named slugs in the invite order and skips ones the hub no longer lists', () => {
|
||||
expect(pinHubProjects(all, { hub_projects: ['c', 'gone', 'a'] }).map((p) => [p.slug, p.pinned])).toEqual([
|
||||
expect(
|
||||
pinHubProjects(all, { hub_projects: ['c', 'gone', 'a'] }).map((p) => [p.slug, p.pinned])
|
||||
).toEqual([
|
||||
['c', true],
|
||||
['a', true],
|
||||
['b', false]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Component } from 'svelte'
|
||||
import { appIconComponent } from '$lib/components/icons'
|
||||
import { appIconComponent } from '$lib/components/icons/appIcon.svelte'
|
||||
import { HubPublishService, SettingService } from '$lib/gen'
|
||||
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
|
||||
import type { ImportProjectSummary } from '$lib/components/ImportProjectCard.svelte'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { BROWSER } from 'esm-env'
|
||||
import { isMac } from '$lib/utils'
|
||||
|
||||
import {
|
||||
AppService,
|
||||
@@ -54,8 +55,8 @@
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { registerToolDisplayActionHandler } from '$lib/components/copilot/chat/createdResourceActions.svelte'
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
import SuperadminSettings from '$lib/components/SuperadminSettings.svelte'
|
||||
import type UserSettings from '$lib/components/UserSettings.svelte'
|
||||
import type SuperadminSettings from '$lib/components/SuperadminSettings.svelte'
|
||||
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
|
||||
import { page } from '$app/state'
|
||||
import FavoriteMenu, {
|
||||
@@ -71,7 +72,7 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import OperatorMenu from '$lib/components/sidebar/OperatorMenu.svelte'
|
||||
import GlobalSearchModal from '$lib/components/search/GlobalSearchModal.svelte'
|
||||
import type GlobalSearchModal from '$lib/components/search/GlobalSearchModal.svelte'
|
||||
import MenuButton from '$lib/components/sidebar/MenuButton.svelte'
|
||||
import MenuLink from '$lib/components/sidebar/MenuLink.svelte'
|
||||
import { loadProtectionRules } from '$lib/workspaceProtectionRules.svelte'
|
||||
@@ -102,7 +103,6 @@
|
||||
import { currentWorkspaceRootId } from '$lib/components/sessions/sessionScope.svelte'
|
||||
import WorkspaceScopeHeader from '$lib/components/sidebar/WorkspaceScopeHeader.svelte'
|
||||
import { DEFAULT_HUB_BASE_URL } from '$lib/hub'
|
||||
import DBManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
|
||||
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
|
||||
import { useIsDarkMode } from '$lib/components/DarkModeObserver.svelte'
|
||||
import { useDbManagerUriState } from '$lib/components/dbManagerDrawerModel.svelte'
|
||||
@@ -686,11 +686,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openSearchModal(
|
||||
text?: string,
|
||||
stack?: import('$lib/components/common/overlayHost.svelte').OverlayStack
|
||||
): void {
|
||||
globalSearchModal?.openSearchWithPrefilledText(text, stack)
|
||||
// Sticky: once opened, a drawer stays mounted, so reopening it is instant.
|
||||
let userSettingsRequested = $state(false)
|
||||
let superadminSettingsRequested = $state(false)
|
||||
$effect(() => {
|
||||
if (page.url.hash.startsWith(USER_SETTINGS_HASH)) userSettingsRequested = true
|
||||
if (page.url.hash === SUPERADMIN_SETTINGS_HASH) superadminSettingsRequested = true
|
||||
})
|
||||
let dbManagerRequested = $state(false)
|
||||
$effect(() => {
|
||||
if (globalDbManagerDrawer.val?.open) dbManagerRequested = true
|
||||
})
|
||||
|
||||
type OverlayStack = import('$lib/components/common/overlayHost.svelte').OverlayStack
|
||||
// The modal is loaded on the first request, which is held here and replayed once it is
|
||||
// bound. Only where the modal mounts (the markup's `{:else if $userStore}`): elsewhere
|
||||
// the request would replay much later.
|
||||
let searchModalRequested = $state(false)
|
||||
let pendingSearchOpen: { text?: string; stack?: OverlayStack } | undefined = $state()
|
||||
let searchModalMounts = $derived(page.status != 404 && !!$userStore)
|
||||
|
||||
function openSearchModal(text?: string, stack?: OverlayStack): void {
|
||||
if (globalSearchModal) globalSearchModal.openSearchWithPrefilledText(text, stack)
|
||||
else if (searchModalMounts) {
|
||||
pendingSearchOpen = { text, stack }
|
||||
searchModalRequested = true
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (globalSearchModal && pendingSearchOpen) {
|
||||
const { text, stack } = pendingSearchOpen
|
||||
pendingSearchOpen = undefined
|
||||
untrack(() => globalSearchModal?.openSearchWithPrefilledText(text, stack))
|
||||
}
|
||||
})
|
||||
|
||||
// Ctrl/Cmd+K is what first loads the modal; once bound, the modal's own listener takes over.
|
||||
function onSearchShortcutBeforeLoad(e: KeyboardEvent) {
|
||||
if (
|
||||
!globalSearchModal &&
|
||||
searchModalMounts &&
|
||||
(isMac() ? e.metaKey : e.ctrlKey) &&
|
||||
e.key === 'k'
|
||||
) {
|
||||
e.preventDefault()
|
||||
openSearchModal()
|
||||
}
|
||||
}
|
||||
|
||||
setContext('openSearchWithPrefilledText', openSearchModal)
|
||||
@@ -921,7 +963,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
<svelte:window bind:innerWidth onkeydown={onSearchShortcutBeforeLoad} />
|
||||
|
||||
<!-- Home + Runs lifted to the top of the workspace nav, sitting with Favorites
|
||||
and Search as the primary quick-access cluster. Excluded from SidebarContent
|
||||
@@ -990,7 +1032,14 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<UserSettings bind:this={userSettings} showMcpMode={true} />
|
||||
<!-- The drawers and modals below are dynamic imports mounted on first open: this layout
|
||||
wraps every workspace page, so a static import gates first paint, and a load on mount
|
||||
competes with the page for bandwidth (several of them reach monaco). -->
|
||||
{#if userSettingsRequested}
|
||||
{#await import('$lib/components/UserSettings.svelte') then UserSettings}
|
||||
<UserSettings.default bind:this={userSettings} showMcpMode={true} />
|
||||
{/await}
|
||||
{/if}
|
||||
{#if accountSetup.pending}
|
||||
<FinishAccountSetup
|
||||
bind:open={accountSetup.open}
|
||||
@@ -1002,9 +1051,15 @@
|
||||
{#if page.status == 404}
|
||||
<CenteredModal title="Page not found, redirecting you to login" loading={true}></CenteredModal>
|
||||
{:else if $userStore}
|
||||
<GlobalSearchModal bind:this={globalSearchModal} />
|
||||
{#if $superadmin}
|
||||
<SuperadminSettings bind:this={superadminSettings} />
|
||||
{#if searchModalRequested}
|
||||
{#await import('$lib/components/search/GlobalSearchModal.svelte') then GlobalSearchModal}
|
||||
<GlobalSearchModal.default bind:this={globalSearchModal} />
|
||||
{/await}
|
||||
{/if}
|
||||
{#if $superadmin && superadminSettingsRequested}
|
||||
{#await import('$lib/components/SuperadminSettings.svelte') then SuperadminSettings}
|
||||
<SuperadminSettings.default bind:this={superadminSettings} />
|
||||
{/await}
|
||||
{/if}
|
||||
{#if mountModal}
|
||||
<CriticalAlertModal bind:muteSettings bind:numUnacknowledgedCriticalAlerts />
|
||||
@@ -1502,8 +1557,10 @@
|
||||
<CenteredModal title="Loading user..." loading={true}></CenteredModal>
|
||||
{/if}
|
||||
|
||||
{#if $workspaceStore && globalDbManagerDrawer.val}
|
||||
<DBManagerDrawer uriState={globalDbManagerDrawer.val} />
|
||||
{#if $workspaceStore && globalDbManagerDrawer.val && dbManagerRequested}
|
||||
{#await import('$lib/components/DBManagerDrawer.svelte') then DBManagerDrawer}
|
||||
<DBManagerDrawer.default uriState={globalDbManagerDrawer.val} />
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
{#if $workspaceStore}
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import { Button, Alert, EmptyState, Skeleton } from '$lib/components/common'
|
||||
import { LoaderCircle, Plus, Webhook } from 'lucide-svelte'
|
||||
import { GithubIcon, GoogleIcon, NextcloudIcon } from '$lib/components/icons'
|
||||
import GithubIcon from '$lib/components/icons/GithubIcon.svelte'
|
||||
import GoogleIcon from '$lib/components/icons/GoogleIcon.svelte'
|
||||
import NextcloudIcon from '$lib/components/icons/NextcloudIcon.svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import { page } from '$app/state'
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { emptyString, parseQueryParams } from '$lib/utils'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import LoginPageHeader from '$lib/components/LoginPageHeader.svelte'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
|
||||
import { clearStores } from '$lib/storeUtils'
|
||||
import { setLicense } from '$lib/enterpriseUtils'
|
||||
import Login from '$lib/components/Login.svelte'
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
import SuperadminSettings from '$lib/components/SuperadminSettings.svelte'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/state'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
@@ -29,6 +29,17 @@
|
||||
|
||||
let { children }: Props = $props()
|
||||
|
||||
// A lazy chunk that failed to load: Chromium, Firefox, Safari, then Vite's CSS preload.
|
||||
const chunkLoadErrors = [
|
||||
'Failed to fetch dynamically imported',
|
||||
'error loading dynamically imported module',
|
||||
'Importing a module script failed',
|
||||
'Unable to preload CSS'
|
||||
]
|
||||
// One failed component rejects once per chunk and stylesheet it needed: one toast per
|
||||
// page, keyed on the path since this layout outlives client-side navigation.
|
||||
let chunkLoadToastPath: string | undefined = undefined
|
||||
|
||||
const monacoEditorUnhandledErrors = [
|
||||
'Model not found',
|
||||
'Connection is disposed.',
|
||||
@@ -200,6 +211,22 @@
|
||||
if (event.reason?.message) {
|
||||
const { message, body, status } = event.reason
|
||||
|
||||
// A chunk gone from the server, typically a tab left open across an upgrade: a
|
||||
// component loaded on demand would otherwise silently never appear.
|
||||
if (chunkLoadErrors.some((m) => message.startsWith(m))) {
|
||||
console.warn(message)
|
||||
// In dev nothing is ever stale: this is a compile error, already on Vite's overlay.
|
||||
if (!import.meta.env.PROD || chunkLoadToastPath === location.pathname) return
|
||||
chunkLoadToastPath = location.pathname
|
||||
sendUserToast(
|
||||
'Part of the page failed to load, Windmill may have been updated',
|
||||
true,
|
||||
[{ label: 'Reload', callback: () => location.reload() }],
|
||||
undefined,
|
||||
15000
|
||||
)
|
||||
return
|
||||
}
|
||||
if (message === 'Missing service editorService') {
|
||||
console.error('Reloading the page to fix a Monaco Editor bug')
|
||||
location.reload()
|
||||
@@ -208,7 +235,6 @@
|
||||
// Unhandled errors from Monaco Editor don't logout the user
|
||||
if (
|
||||
monacoEditorUnhandledErrors.includes(message) ||
|
||||
message.startsWith('Failed to fetch dynamically imported') ||
|
||||
message.startsWith('Unable to figure out browser width and height') ||
|
||||
message.startsWith('Unable to read file') ||
|
||||
message.startsWith('Could not find source file')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// always fails with the credentials error, and the provider buttons don't navigate.
|
||||
import Login, { type LoginPreview } from '$lib/components/Login.svelte'
|
||||
import LoginHeading from '$lib/components/LoginHeading.svelte'
|
||||
import { WindmillIcon } from '$lib/components/icons'
|
||||
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
|
||||
+73
-24
@@ -77,6 +77,36 @@ function assertAcyclicChunks() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail the build if a chunk statically reachable from one containing `entry` contains a module
|
||||
* whose id includes one of `forbidden`. Only static imports count: a dynamic import is the
|
||||
* fix, not the problem.
|
||||
*/
|
||||
function assertStaticClosureExcludes(ctx, bundle, entry, forbidden, hint) {
|
||||
const chunks = Object.entries(bundle).filter(([, c]) => c.type === 'chunk')
|
||||
const idsOf = (c) => c.moduleIds ?? Object.keys(c.modules ?? {})
|
||||
const starts = chunks
|
||||
.filter(([, c]) => idsOf(c).some((id) => id.includes(entry)))
|
||||
.map(([file]) => file)
|
||||
if (!starts.length) ctx.error(`No chunk contains ${entry}; update ${hint}`)
|
||||
const seen = new Set(starts)
|
||||
const queue = [...starts]
|
||||
while (queue.length) {
|
||||
const chunk = bundle[queue.shift()]
|
||||
if (!chunk) continue
|
||||
const hit = idsOf(chunk).find((id) => forbidden.some((f) => id.includes(f)))
|
||||
if (hit) ctx.error(`${entry} statically loads ${hit}; import it lazily (see ${hint})`)
|
||||
for (const dep of chunk.imports ?? []) {
|
||||
if (seen.has(dep)) continue
|
||||
seen.add(dep)
|
||||
queue.push(dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isClientBuild = (bundle) =>
|
||||
Object.keys(bundle).some((file) => file.startsWith('_app/immutable/'))
|
||||
|
||||
/**
|
||||
* Fail the build if a public app URL statically loads the low-code runtime or monaco.
|
||||
*
|
||||
@@ -94,30 +124,36 @@ function assertLeanPublicAppRoutes() {
|
||||
return {
|
||||
name: 'wm-assert-lean-public-app-routes',
|
||||
generateBundle(_options, bundle) {
|
||||
const chunks = Object.entries(bundle).filter(([, c]) => c.type === 'chunk')
|
||||
if (!chunks.some(([file]) => file.startsWith('_app/immutable/'))) return
|
||||
const idsOf = (c) => c.moduleIds ?? Object.keys(c.modules ?? {})
|
||||
if (!isClientBuild(bundle)) return
|
||||
for (const route of routes) {
|
||||
const starts = chunks
|
||||
.filter(([, c]) => idsOf(c).some((id) => id.includes(route)))
|
||||
.map(([file]) => file)
|
||||
if (!starts.length)
|
||||
this.error(`No chunk contains ${route}; update assertLeanPublicAppRoutes`)
|
||||
const seen = new Set(starts)
|
||||
const queue = [...starts]
|
||||
while (queue.length) {
|
||||
const chunk = bundle[queue.shift()]
|
||||
if (!chunk) continue
|
||||
const hit = idsOf(chunk).find((id) => forbidden.some((f) => id.includes(f)))
|
||||
if (hit) {
|
||||
this.error(`${route} statically loads ${hit}; import it lazily (see loadAppPreview.ts)`)
|
||||
}
|
||||
for (const dep of chunk.imports ?? []) {
|
||||
if (seen.has(dep)) continue
|
||||
seen.add(dep)
|
||||
queue.push(dep)
|
||||
}
|
||||
}
|
||||
assertStaticClosureExcludes(this, bundle, route, forbidden, 'loadAppPreview.ts')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail the build if the app shell or the home page statically loads monaco.
|
||||
*
|
||||
* The layouts below wrap every page, so what they import statically is downloaded before
|
||||
* any page renders, and the home page is where most sessions land; monaco is several MB.
|
||||
* Editors, drawers and modals that reach it are dynamic imports mounted on first use (see
|
||||
* the `(logged)` layout).
|
||||
*/
|
||||
function assertMonacoFreeAppShell() {
|
||||
const entries = [
|
||||
'/src/routes/+layout.svelte',
|
||||
'/src/routes/(root)/+layout.svelte',
|
||||
'/src/routes/(root)/(logged)/+layout.svelte',
|
||||
'/src/routes/(root)/(logged)/+page.svelte'
|
||||
]
|
||||
const forbidden = ['/node_modules/monaco-editor/', '/node_modules/@codingame/monaco-vscode-']
|
||||
return {
|
||||
name: 'wm-assert-monaco-free-app-shell',
|
||||
generateBundle(_options, bundle) {
|
||||
if (!isClientBuild(bundle)) return
|
||||
for (const entry of entries) {
|
||||
assertStaticClosureExcludes(this, bundle, entry, forbidden, 'the (logged) layout')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +244,18 @@ const config = {
|
||||
'public.windmill.xyz'
|
||||
],
|
||||
port: parseInt(process.env.FRONTEND_PORT) || 3000,
|
||||
// Transform the app shell's module graph at startup rather than on the first page
|
||||
// load, which otherwise sits on the loading screen for ~15s (Tailwind's full content
|
||||
// scan for app.css alone takes several seconds).
|
||||
warmup: {
|
||||
clientFiles: [
|
||||
'./src/lib/assets/app.css',
|
||||
'./src/routes/+layout.svelte',
|
||||
'./src/routes/(root)/+layout.svelte',
|
||||
'./src/routes/(root)/(logged)/+layout.svelte',
|
||||
'./src/routes/(root)/(logged)/+page.svelte'
|
||||
]
|
||||
},
|
||||
cors: { origin: '*' },
|
||||
// `windmill-chat` (svelte.config.js alias) lives outside the frontend root.
|
||||
fs: {
|
||||
@@ -279,7 +327,8 @@ const config = {
|
||||
...(process.env.HTTPS === 'true' ? [mkcert()] : []),
|
||||
plugin,
|
||||
assertAcyclicChunks(),
|
||||
assertLeanPublicAppRoutes()
|
||||
assertLeanPublicAppRoutes(),
|
||||
assertMonacoFreeAppShell()
|
||||
],
|
||||
define: { '__pkg__.version': JSON.stringify(version) },
|
||||
optimizeDeps: {
|
||||
|
||||
Reference in New Issue
Block a user