Files
windmill/frontend/src/lib/components/ResourceForm.svelte
T
GuilhemandClaude Opus 4.8 c000bbca28 fix(frontend): scope raw-app, flow and script editors to the session workspace (#10015)
* fix(frontend): scope raw-app/flow/script editors to the session workspace

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): scope flow and script editor operations to the session workspace

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Codex review — thread session workspace through flow resource pickers, script fetch, preview cancel/recording and path collision check

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Pi review — show acting workspace in script-not-found message and fetch picked script from it in EditorBar

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Codex review round 2 — thread session workspace into flow step test, raw-app inline runnable, inline editor toolbars and MCP OAuth path

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Codex review round 3 — thread session workspace into dynamic-input helpers and the flow-preview argument side panel (history/saved-inputs/captures)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Codex review round 4 — thread session workspace into nested flow/script drawers, flow chat inputs and the flow input side tabs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Codex review round 5 — thread session workspace into script-module fork/reload and key the raw-app schema cache by workspace

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Codex review round 6 — key the DB manager schema cache by acting workspace

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): address Codex review round 7 — thread session workspace into resource-valued arg pickers and the editor variable/resource helper drawers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: thread acting workspace through flow asset explore controls

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:53:43 +02:00

313 lines
9.0 KiB
Svelte

<script lang="ts">
import type { Schema } from '$lib/common'
import type { Resource, ResourceType } from '$lib/gen'
import { emptyString, isOwner, urlize } from '$lib/utils'
import { Alert, Skeleton } from './common'
import Path from './Path.svelte'
import LabelsInput from './LabelsInput.svelte'
import Required from './Required.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import SchemaForm from './SchemaForm.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import FilesetEditor from './FilesetEditor.svelte'
import Toggle from './Toggle.svelte'
import TestConnection from './TestConnection.svelte'
import { Pen } from 'lucide-svelte'
import Markdown from 'svelte-exmarkdown'
import autosize from '$lib/autosize'
import GfmMarkdown from './GfmMarkdown.svelte'
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
import Button from './common/button/Button.svelte'
import ResourceGen from './copilot/ResourceGen.svelte'
import SyncResourceTypes from './SyncResourceTypes.svelte'
import Label from './Label.svelte'
interface Props {
path: string
initialPath: string
hidePath?: boolean
labels: string[] | undefined
description: string
args: Record<string, any>
wsSpecific: boolean
isValid: boolean
viewJsonSchema: boolean
jsonError: string
deployTo: string | undefined
can_write: boolean
resource_type: string | undefined
resourceTypeInfo: ResourceType | undefined
resourceSchema: Schema | undefined
loadingSchema: boolean
resourceToEdit: Resource | undefined
onLoadResourceType?: () => void
/** Workspace the path is validated against and the connection is tested in;
* defaults to the nav workspace. */
workspace?: string | undefined
}
let {
path = $bindable(),
initialPath,
hidePath = false,
labels = $bindable(),
description = $bindable(),
args = $bindable(),
wsSpecific = $bindable(),
isValid = $bindable(),
viewJsonSchema = $bindable(),
jsonError = $bindable(),
deployTo,
can_write,
resource_type,
resourceTypeInfo,
resourceSchema,
loadingSchema,
resourceToEdit,
onLoadResourceType,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let editDescription = $state(false)
let rawCode: string | undefined = $state(undefined)
let textFileContent: string = $state('')
function parseJson() {
try {
args = JSON.parse(rawCode ?? '')
jsonError = ''
} catch (e) {
jsonError = e.message
}
}
function parseTextFileContent() {
args = { content: textFileContent }
}
// The raw JSON editor is the active input whenever the "As JSON" toggle is on,
// or no schema-based form can be rendered (e.g. the resource type is missing
// from the workspace). In both cases rawCode must be seeded from args.
let usesRawEditor = $derived(
!loadingSchema &&
(viewJsonSchema ||
(!resourceTypeInfo?.is_fileset && !(resourceSchema && resourceSchema.properties)))
)
$effect(() => {
if (rawCode !== undefined) parseJson()
})
$effect(() => {
if (usesRawEditor && rawCode === undefined) {
rawCode = JSON.stringify(args, null, 2)
}
})
// Seed the JSON editor when the resource type schema is missing
// (restores the old ResourceEditor's catch-block behavior)
$effect(() => {
if (resource_type && !loadingSchema && !resourceSchema && rawCode === undefined) {
rawCode = JSON.stringify(args, null, 2)
}
})
$effect(() => {
if (textFileContent) parseTextFileContent()
})
$effect(() => {
if (resourceTypeInfo?.format_extension && !resourceTypeInfo?.is_fileset && !viewJsonSchema) {
textFileContent = args?.content ?? ''
}
})
</script>
{#if !hidePath}
<div>
{#if !can_write}
<div class="my-2">
<Alert type="warning" title="Only read access">
You only have read access to this resource and cannot edit it
</Alert>
</div>
{/if}
<Label label="Path">
<Path
disabled={initialPath != '' && !isOwner(initialPath, $userStore, ws)}
bind:path
{initialPath}
namePlaceholder="resource"
kind="resource"
workspaceOverride={workspace}
/>
</Label>
</div>
{/if}
<LabelsInput bind:labels class="-mt-4" />
{#if deployTo}
<Label
label="Workspace specific"
tooltip="Prevents this resource from being deployed to prod/staging. When enabled, any variable referenced via $var: inside the resource value is also automatically marked workspace-specific. Disabling this toggle does not un-mark those variables — they may be referenced by other resources."
>
<Toggle bind:checked={wsSpecific} />
</Label>
{/if}
{#if !emptyString(resourceTypeInfo?.description)}
<div class="flex flex-col gap-1">
<h4 class="text-xs text-emphasis font-semibold">{resourceTypeInfo?.name} description</h4>
<div class="text-xs text-primary font-normal">
<Markdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} />
</div>
</div>
{/if}
<div class="flex flex-col gap-1">
<h4 class="inline-flex items-center gap-2 text-xs text-emphasis font-semibold"
>Resource description <Required required={false} />
{#if can_write}
<Button
variant="subtle"
unifiedSize="xs"
btnClasses={editDescription ? 'bg-surface-hover' : ''}
startIcon={{ icon: Pen }}
on:click={() => (editDescription = !editDescription)}
/>
{/if}
</h4>
{#if can_write && editDescription}
<div class="relative">
<div class="text-2xs text-primary absolute -top-4 right-0">GH Markdown</div>
<textarea
class="text-xs text-primary font-normal"
disabled={!can_write}
use:autosize
bind:value={description}
placeholder="Describe what this resource is for"
></textarea>
</div>
{:else if description == undefined || description == ''}
<div class="text-xs text-secondary font-normal">No description provided</div>
{:else}
<div class="text-xs text-primary font-normal">
<GfmMarkdown md={description} noPadding />
</div>
{/if}
</div>
<div class="flex flex-col gap-1">
<div class="w-full flex gap-4 flex-row-reverse items-center">
<Toggle
bind:checked={viewJsonSchema}
on:change={(e) => {
if (e.detail) {
rawCode = JSON.stringify(args, null, 2)
} else if (resourceTypeInfo?.format_extension && !resourceTypeInfo?.is_fileset) {
textFileContent = args?.content ?? ''
}
}}
options={{
right: 'As JSON'
}}
/>
<ResourceGen
bind:args
resourceType={resource_type}
resourceName={path}
resourceDescription={description}
{resourceSchema}
/>
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
{:else}
<TestConnection
resourceType={resourceToEdit?.resource_type}
{args}
workspaceOverride={workspace}
/>
{/if}
{#if resource_type === 'git_repository' && $workspaceStore && ($userStore?.is_admin || $userStore?.is_super_admin)}
<GitHubAppIntegration
resourceType={resource_type}
{args}
{description}
onArgsUpdate={(newArgs) => {
args = newArgs
if (viewJsonSchema) {
rawCode = JSON.stringify(args, null, 2)
}
}}
onDescriptionUpdate={(newDescription) => (description = newDescription)}
/>
{/if}
</div>
<div>
{#if loadingSchema}
<Skeleton layout={[[4]]} />
{:else if !viewJsonSchema && resourceTypeInfo?.is_fileset}
<div class="mt-1 flex items-center gap-2">
<h5 class="inline-flex items-center gap-4">Fileset</h5>
<ResourceGen
bind:args
resourceType={resource_type}
resourceName={path}
resourceDescription={description}
{resourceSchema}
isFileset
/>
</div>
<FilesetEditor bind:args />
{:else if !viewJsonSchema && resourceSchema && resourceSchema?.properties}
{#if resourceTypeInfo?.format_extension}
<h5 class="mt-1 inline-flex items-center gap-4">
File content ({resourceTypeInfo.format_extension})
</h5>
<div class="">
<SimpleEditor
autoHeight
lang={resourceTypeInfo.format_extension}
bind:code={textFileContent}
fixedOverflowWidgets={false}
/>
</div>
{:else}
<SchemaForm
onlyMaskPassword
noDelete
disabled={!can_write}
compact
schema={resourceSchema}
bind:args
bind:isValid
{workspace}
/>
{/if}
{:else if !can_write}
<input type="text" disabled value={rawCode} />
{:else}
{#if !viewJsonSchema}
<div class="flex flex-col gap-2 mb-4">
<p class="text-red-500 dark:text-red-400 text-xs">
Resource type '{resource_type}' not found in your workspace
</p>
<SyncResourceTypes onSynced={() => onLoadResourceType?.()} />
<p class="italic text-secondary text-xs"> Define the value in JSON directly </p>
</div>
{/if}
{#if !emptyString(jsonError)}<span class="text-red-400 text-xs mb-1 flex flex-row-reverse"
>{jsonError}</span
>{:else}<div class="py-2"></div>{/if}
<div class="bg-surface-tertiary rounded-md border py-2.5">
<SimpleEditor autoHeight lang="json" bind:code={rawCode} />
</div>
{/if}
</div>
</div>